Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c38aaebf3 | ||
|
|
18b0ac44bf | ||
|
|
b6749eff2f | ||
|
|
c73a103280 | ||
|
|
a5d194f730 | ||
|
|
6320436b88 | ||
|
|
87325927ed | ||
|
|
4435cdf5b6 | ||
|
|
b041654611 | ||
|
|
e833dcefd4 | ||
|
|
29680f0ee8 | ||
|
|
670329700c | ||
|
|
62e17af311 | ||
|
|
e3938a2c85 | ||
|
|
49ffc3d99e | ||
|
|
34bb1c34a2 | ||
|
|
b27a56a296 | ||
|
|
ecd7095123 | ||
|
|
d449513861 | ||
|
|
6709e229b3 | ||
|
|
00a8f16262 | ||
|
|
00d16f8615 | ||
|
|
c90676d2b2 | ||
|
|
b89c011ad5 | ||
|
|
c3de31aa88 | ||
|
|
b6df0be865 | ||
|
|
a89f84f76e | ||
|
|
5a916cc221 | ||
|
|
dcf421ff93 | ||
|
|
d655258311 | ||
|
|
f6d13d4318 | ||
|
|
d5350d7c30 | ||
|
|
c63fd04c92 | ||
|
|
64b418a551 | ||
|
|
36c9c84047 | ||
|
|
88b4aaa301 | ||
|
|
eac4c227c9 | ||
|
|
d5eb7d4a55 | ||
|
|
80b15e16e2 | ||
|
|
cfacedbb1a | ||
|
|
c3bab033ed | ||
|
|
524f9bd84f | ||
|
|
4658ede9d6 | ||
|
|
f7b463aca1 | ||
|
|
c1a6e92b1d | ||
|
|
eefb0e427e | ||
|
|
c23d81b740 | ||
|
|
6dac231040 | ||
|
|
6fd3e531c3 | ||
|
|
c1c05991cf | ||
|
|
ab378e14d1 | ||
|
|
c0f9bf9ef5 | ||
|
|
bc6a7b3f20 | ||
|
|
036b0823b9 | ||
|
|
be99595bde | ||
|
|
01836a4b4c | ||
|
|
9f3b3dd773 | ||
|
|
3210b9e752 | ||
|
|
2d66b8bf1d | ||
|
|
f3d6afead1 | ||
|
|
1690b58c67 | ||
|
|
2e8eda5c67 | ||
|
|
74a4dc2533 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,3 +8,7 @@ src/version.h
|
|||||||
dev-config/
|
dev-config/
|
||||||
db/
|
db/
|
||||||
copy_executable_local.sh
|
copy_executable_local.sh
|
||||||
|
nostr_login_lite/
|
||||||
|
style_guide/
|
||||||
|
nostr-tools
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
|||||||
[submodule "nostr_core_lib"]
|
[submodule "nostr_core_lib"]
|
||||||
path = nostr_core_lib
|
path = nostr_core_lib
|
||||||
url = https://git.laantungir.net/laantungir/nostr_core_lib.git
|
url = https://git.laantungir.net/laantungir/nostr_core_lib.git
|
||||||
|
[submodule "c_utils_lib"]
|
||||||
|
path = c_utils_lib
|
||||||
|
url = ssh://git@git.laantungir.net:2222/laantungir/c_utils_lib.git
|
||||||
|
|||||||
298
.roo/architect/AGENTS.md
Normal file
298
.roo/architect/AGENTS.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
|
||||||
|
# AGENTS.md - AI Agent Integration Guide for Architect Mode
|
||||||
|
|
||||||
|
**Project-Specific Information for AI Agents Working with C-Relay in Architect Mode**
|
||||||
|
|
||||||
|
## Critical Architecture Understanding
|
||||||
|
|
||||||
|
### System Architecture Overview
|
||||||
|
C-Relay implements a **unique event-based configuration architecture** that fundamentally differs from traditional Nostr relays:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ WebSocket │ │ Configuration │ │ Database │
|
||||||
|
│ + HTTP │◄──►│ Event System │◄──►│ (SQLite) │
|
||||||
|
│ (Port 8888) │ │ (Kind 33334) │ │ Schema v4 │
|
||||||
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ nostr_core_lib │ │ Admin Key │ │ Event Storage │
|
||||||
|
│ (Crypto/Sigs) │ │ Management │ │ + Subscriptions │
|
||||||
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Architectural Principles
|
||||||
|
|
||||||
|
#### 1. Event-Driven Configuration
|
||||||
|
**Design Philosophy**: Configuration as cryptographically signed events rather than files
|
||||||
|
- **Benefits**: Auditability, remote management, tamper-evidence
|
||||||
|
- **Trade-offs**: Complexity in configuration changes, admin key management burden
|
||||||
|
- **Implementation**: Kind 33334 events stored in same database as relay events
|
||||||
|
|
||||||
|
#### 2. Identity-Based Database Naming
|
||||||
|
**Design Philosophy**: Database file named by relay's generated public key
|
||||||
|
- **Benefits**: Prevents database conflicts, enables multi-relay deployments
|
||||||
|
- **Trade-offs**: Cannot predict database filename, complicates backup strategies
|
||||||
|
- **Implementation**: `<relay_pubkey>.db` created in build/ directory
|
||||||
|
|
||||||
|
#### 3. Single-Binary Deployment
|
||||||
|
**Design Philosophy**: All functionality embedded in one executable
|
||||||
|
- **Benefits**: Simple deployment, no external dependencies to manage
|
||||||
|
- **Trade-offs**: Larger binary size, harder to modularize
|
||||||
|
- **Implementation**: SQL schema embedded as header file, nostr_core_lib as submodule
|
||||||
|
|
||||||
|
#### 4. Dual-Protocol Support
|
||||||
|
**Design Philosophy**: WebSocket (Nostr) and HTTP (NIP-11) on same port
|
||||||
|
- **Benefits**: Simplified port management, reduced infrastructure complexity
|
||||||
|
- **Trade-offs**: Protocol detection overhead, libwebsockets dependency
|
||||||
|
- **Implementation**: Request routing based on HTTP headers and upgrade requests
|
||||||
|
|
||||||
|
## Architectural Decision Analysis
|
||||||
|
|
||||||
|
### Configuration System Design
|
||||||
|
**Traditional Approach vs C-Relay:**
|
||||||
|
```
|
||||||
|
Traditional: C-Relay:
|
||||||
|
config.json → kind 33334 events
|
||||||
|
ENV variables → cryptographically signed tags
|
||||||
|
File watching → database polling/restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implications for Extensions:**
|
||||||
|
- Configuration changes require event signing capabilities
|
||||||
|
- No hot-reloading without architectural changes
|
||||||
|
- Admin key loss = complete database reset required
|
||||||
|
|
||||||
|
### Database Architecture Decisions
|
||||||
|
**Schema Design Philosophy:**
|
||||||
|
- **Event Tags as JSON**: Separate table with JSON column instead of normalized relations
|
||||||
|
- **Application-Level Filtering**: NIP-40 expiration handled in C, not SQL
|
||||||
|
- **Embedded Schema**: Version 4 schema compiled into binary
|
||||||
|
|
||||||
|
**Scaling Considerations:**
|
||||||
|
- SQLite suitable for small-to-medium relays (< 10k concurrent connections)
|
||||||
|
- Single-writer limitation of SQLite affects write-heavy workloads
|
||||||
|
- JSON tag storage optimizes for read performance over write normalization
|
||||||
|
|
||||||
|
### Memory Management Architecture
|
||||||
|
**Thread Safety Model:**
|
||||||
|
- Global subscription manager with mutex protection
|
||||||
|
- Per-client subscription limits enforced in memory
|
||||||
|
- WebSocket connection state managed by libwebsockets
|
||||||
|
|
||||||
|
**Resource Management:**
|
||||||
|
- JSON objects use reference counting (jansson library)
|
||||||
|
- String duplication pattern for configuration values
|
||||||
|
- Automatic cleanup on client disconnect
|
||||||
|
|
||||||
|
## Architectural Extension Points
|
||||||
|
|
||||||
|
### Adding New Configuration Options
|
||||||
|
**Required Changes:**
|
||||||
|
1. Update [`default_config_event.h`](src/default_config_event.h) template
|
||||||
|
2. Add parsing logic in [`config.c`](src/config.c) `load_config_from_database()`
|
||||||
|
3. Add global config struct field in [`config.h`](src/config.h)
|
||||||
|
4. Update documentation in [`docs/configuration_guide.md`](docs/configuration_guide.md)
|
||||||
|
|
||||||
|
### Adding New NIP Support
|
||||||
|
**Integration Pattern:**
|
||||||
|
1. Event validation in [`request_validator.c`](src/request_validator.c)
|
||||||
|
2. Protocol handling in [`main.c`](src/main.c) WebSocket callback
|
||||||
|
3. Database storage considerations in schema
|
||||||
|
4. Add test in `tests/` directory
|
||||||
|
|
||||||
|
### Scaling Architecture
|
||||||
|
**Current Limitations:**
|
||||||
|
- Single process, no horizontal scaling
|
||||||
|
- SQLite single-writer bottleneck
|
||||||
|
- Memory-based subscription management
|
||||||
|
|
||||||
|
**Potential Extensions:**
|
||||||
|
- Redis for subscription state sharing
|
||||||
|
- PostgreSQL for better concurrent write performance
|
||||||
|
- Load balancer for read scaling with multiple instances
|
||||||
|
|
||||||
|
## Deployment Architecture Patterns
|
||||||
|
|
||||||
|
### Development Deployment
|
||||||
|
```
|
||||||
|
Developer Machine:
|
||||||
|
├── ./make_and_restart_relay.sh
|
||||||
|
├── build/c_relay_x86
|
||||||
|
├── build/<relay_pubkey>.db
|
||||||
|
└── relay.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production SystemD Deployment
|
||||||
|
```
|
||||||
|
/opt/c-relay/:
|
||||||
|
├── c_relay_x86
|
||||||
|
├── <relay_pubkey>.db
|
||||||
|
├── systemd service (c-relay.service)
|
||||||
|
└── c-relay user isolation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Deployment Architecture
|
||||||
|
```
|
||||||
|
Container:
|
||||||
|
├── Multi-stage build (deps + binary)
|
||||||
|
├── Volume mount for database persistence
|
||||||
|
├── Health checks via NIP-11 endpoint
|
||||||
|
└── Signal handling for graceful shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse Proxy Architecture
|
||||||
|
```
|
||||||
|
Internet → Nginx/HAProxy → C-Relay
|
||||||
|
├── WebSocket upgrade handling
|
||||||
|
├── SSL termination
|
||||||
|
└── Rate limiting
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Architecture Considerations
|
||||||
|
|
||||||
|
### Key Management Design
|
||||||
|
**Admin Key Security Model:**
|
||||||
|
- Generated once, displayed once, never stored
|
||||||
|
- Required for all configuration changes
|
||||||
|
- Loss requires complete database reset
|
||||||
|
|
||||||
|
**Relay Identity Model:**
|
||||||
|
- Separate keypair for relay identity
|
||||||
|
- Public key used for database naming
|
||||||
|
- Private key never exposed to clients
|
||||||
|
|
||||||
|
### Event Validation Pipeline
|
||||||
|
```
|
||||||
|
WebSocket Input → JSON Parse → Schema Validate → Signature Verify → Store
|
||||||
|
↓ ↓ ↓
|
||||||
|
reject reject reject success
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attack Surface Analysis
|
||||||
|
**Network Attack Vectors:**
|
||||||
|
- WebSocket connection flooding (mitigated by libwebsockets limits)
|
||||||
|
- JSON parsing attacks (handled by jansson library bounds checking)
|
||||||
|
- SQLite injection (prevented by prepared statements)
|
||||||
|
|
||||||
|
**Configuration Attack Vectors:**
|
||||||
|
- Admin key compromise (complete relay control)
|
||||||
|
- Event signature forgery (prevented by nostr_core_lib validation)
|
||||||
|
- Replay attacks (event timestamp validation required)
|
||||||
|
|
||||||
|
## Non-Obvious Architectural Considerations
|
||||||
|
|
||||||
|
### Database Evolution Strategy
|
||||||
|
**Current Limitations:**
|
||||||
|
- Schema changes require database recreation
|
||||||
|
- No migration system for configuration events
|
||||||
|
- Version 4 schema embedded in binary
|
||||||
|
|
||||||
|
**Future Architecture Needs:**
|
||||||
|
- Schema versioning and migration system
|
||||||
|
- Backward compatibility for configuration events
|
||||||
|
- Database backup/restore procedures
|
||||||
|
|
||||||
|
### Configuration Event Lifecycle
|
||||||
|
**Event Flow:**
|
||||||
|
```
|
||||||
|
Admin Signs Event → WebSocket Submit → Validate → Store → Restart Required
|
||||||
|
↓ ↓ ↓
|
||||||
|
Signature Check Database Config Reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architectural Implications:**
|
||||||
|
- No hot configuration reloading
|
||||||
|
- Configuration changes require planned downtime
|
||||||
|
- Event ordering matters for multiple simultaneous changes
|
||||||
|
|
||||||
|
### Cross-Architecture Deployment
|
||||||
|
**Build System Architecture:**
|
||||||
|
- Auto-detection of host architecture
|
||||||
|
- Cross-compilation support for ARM64
|
||||||
|
- Architecture-specific binary outputs
|
||||||
|
|
||||||
|
**Deployment Implications:**
|
||||||
|
- Binary must match target architecture
|
||||||
|
- Dependencies must be available for target architecture
|
||||||
|
- Debug tooling architecture-specific
|
||||||
|
|
||||||
|
### Performance Architecture Characteristics
|
||||||
|
**Bottlenecks:**
|
||||||
|
1. **SQLite Write Performance**: Single writer limitation
|
||||||
|
2. **JSON Parsing**: Per-event parsing overhead
|
||||||
|
3. **Signature Validation**: Cryptographic operations per event
|
||||||
|
4. **Memory Management**: JSON object lifecycle management
|
||||||
|
|
||||||
|
**Optimization Points:**
|
||||||
|
- Prepared statement reuse
|
||||||
|
- Connection pooling for concurrent reads
|
||||||
|
- Event batching for bulk operations
|
||||||
|
- Subscription indexing strategies
|
||||||
|
|
||||||
|
### Integration Architecture Patterns
|
||||||
|
**Monitoring Integration:**
|
||||||
|
- NIP-11 endpoint for health checks
|
||||||
|
- Log file monitoring for operational metrics
|
||||||
|
- Database query monitoring for performance
|
||||||
|
- Process monitoring for resource usage
|
||||||
|
|
||||||
|
**Backup Architecture:**
|
||||||
|
- Database file backup (SQLite file copy)
|
||||||
|
- Configuration event export/import
|
||||||
|
- Admin key secure storage (external to relay)
|
||||||
|
|
||||||
|
### Future Extension Architectures
|
||||||
|
**Multi-Relay Coordination:**
|
||||||
|
- Database sharding by event kind
|
||||||
|
- Cross-relay event synchronization
|
||||||
|
- Distributed configuration management
|
||||||
|
|
||||||
|
**Plugin Architecture Possibilities:**
|
||||||
|
- Event processing pipeline hooks
|
||||||
|
- Custom validation plugins
|
||||||
|
- External authentication providers
|
||||||
|
|
||||||
|
**Scaling Architecture Options:**
|
||||||
|
- Read replicas with PostgreSQL migration
|
||||||
|
- Event stream processing with message queues
|
||||||
|
- Microservice decomposition (auth, storage, validation)
|
||||||
|
|
||||||
|
## Architectural Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
1. **Configuration File Addition**: Breaks event-based config paradigm
|
||||||
|
2. **Direct Database Modification**: Bypasses signature validation
|
||||||
|
3. **Hard-Coded Ports**: Conflicts with auto-fallback system
|
||||||
|
4. **Schema Modifications**: Requires database recreation
|
||||||
|
5. **Admin Key Storage**: Violates security model
|
||||||
|
6. **Blocking Operations**: Interferes with WebSocket event loop
|
||||||
|
7. **Memory Leaks**: JSON objects must be properly reference counted
|
||||||
|
8. **Thread Unsafe Operations**: Global state requires proper synchronization
|
||||||
|
|
||||||
|
## Architecture Decision Records (Implicit)
|
||||||
|
|
||||||
|
### Decision: Event-Based Configuration
|
||||||
|
**Context**: Traditional config files vs. cryptographic auditability
|
||||||
|
**Decision**: Store configuration as signed Nostr events
|
||||||
|
**Consequences**: Complex configuration changes, enhanced security, remote management capability
|
||||||
|
|
||||||
|
### Decision: SQLite Database
|
||||||
|
**Context**: Database choice for relay storage
|
||||||
|
**Decision**: Embedded SQLite with JSON tag storage
|
||||||
|
**Consequences**: Simple deployment, single-writer limitation, application-level filtering
|
||||||
|
|
||||||
|
### Decision: Single Binary Deployment
|
||||||
|
**Context**: Dependency management vs. deployment simplicity
|
||||||
|
**Decision**: Embed all dependencies and schema in binary
|
||||||
|
**Consequences**: Larger binary, simple deployment, version coupling
|
||||||
|
|
||||||
|
### Decision: Dual Protocol Support
|
||||||
|
**Context**: WebSocket for Nostr, HTTP for NIP-11
|
||||||
|
**Decision**: Same port serves both protocols
|
||||||
|
**Consequences**: Simplified deployment, protocol detection overhead, libwebsockets dependency
|
||||||
|
|
||||||
|
These architectural decisions form the foundation of C-Relay's unique approach to Nostr relay implementation and should be carefully considered when planning extensions or modifications.
|
||||||
|
**
|
||||||
|
|
||||||
|
[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
description: "Brief description of what this command does"
|
description: "Brief description of what this command does"
|
||||||
---
|
---
|
||||||
|
|
||||||
Run build_and_push.sh, and supply a good git commit message.
|
Run increment_and_push.sh, and supply a good git commit message. For example:
|
||||||
|
|
||||||
|
./increment_and_push.sh "Fixed the bug with nip05 implementation"
|
||||||
1
.rooignore
Normal file
1
.rooignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
src/embedded_web_content.c
|
||||||
152
AGENTS.md
Normal file
152
AGENTS.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# AGENTS.md - AI Agent Integration Guide
|
||||||
|
|
||||||
|
**Project-Specific Information for AI Agents Working with C-Relay**
|
||||||
|
|
||||||
|
## Critical Build Commands
|
||||||
|
|
||||||
|
### Primary Build Command
|
||||||
|
```bash
|
||||||
|
./make_and_restart_relay.sh
|
||||||
|
```
|
||||||
|
**Never use `make` directly.** The project requires the custom restart script which:
|
||||||
|
- Handles database preservation/cleanup based on flags
|
||||||
|
- Manages architecture-specific binary detection (x86/ARM64)
|
||||||
|
- Performs automatic process cleanup and port management
|
||||||
|
- Starts relay in background with proper logging
|
||||||
|
|
||||||
|
### Architecture-Specific Binary Outputs
|
||||||
|
- **x86_64**: `./build/c_relay_x86`
|
||||||
|
- **ARM64**: `./build/c_relay_arm64`
|
||||||
|
- **Other**: `./build/c_relay_$(ARCH)`
|
||||||
|
|
||||||
|
### Database File Naming Convention
|
||||||
|
- **Format**: `<relay_pubkey>.db` (NOT `.nrdb` as shown in docs)
|
||||||
|
- **Location**: Created in `build/` directory during execution
|
||||||
|
- **Cleanup**: Use `--preserve-database` flag to retain between builds
|
||||||
|
|
||||||
|
## Critical Integration Issues
|
||||||
|
|
||||||
|
### Event-Based Configuration System
|
||||||
|
- **No traditional config files** - all configuration stored in config table
|
||||||
|
- Admin private key shown **only once** on first startup
|
||||||
|
- Configuration changes require cryptographically signed events
|
||||||
|
- Database path determined by generated relay pubkey
|
||||||
|
|
||||||
|
### First-Time Startup Sequence
|
||||||
|
1. Relay generates admin keypair and relay keypair
|
||||||
|
2. Creates database file with relay pubkey as filename
|
||||||
|
3. Stores default configuration in config table
|
||||||
|
4. **CRITICAL**: Admin private key displayed once and never stored on disk
|
||||||
|
|
||||||
|
### Port Management
|
||||||
|
- Default port 8888 with automatic fallback (8889, 8890, etc.)
|
||||||
|
- Script performs port availability checking before libwebsockets binding
|
||||||
|
- Process cleanup includes force-killing processes on port 8888
|
||||||
|
|
||||||
|
### Database Schema Dependencies
|
||||||
|
- Uses embedded SQL schema (`sql_schema.h`)
|
||||||
|
- Schema version 4 with JSON tag storage
|
||||||
|
- **Critical**: Event expiration filtering done at application level, not SQL level
|
||||||
|
|
||||||
|
### Admin API Event Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [
|
||||||
|
["p", "<relay_pubkey>"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Commands** (encrypted in content):
|
||||||
|
- `["relay_description", "My Relay"]`
|
||||||
|
- `["max_subscriptions_per_client", "25"]`
|
||||||
|
- `["pow_min_difficulty", "16"]`
|
||||||
|
|
||||||
|
**Auth Rule Commands** (encrypted in content):
|
||||||
|
- `["blacklist", "pubkey", "hex_pubkey_value"]`
|
||||||
|
- `["whitelist", "pubkey", "hex_pubkey_value"]`
|
||||||
|
|
||||||
|
**Query Commands** (encrypted in content):
|
||||||
|
- `["auth_query", "all"]`
|
||||||
|
- `["system_command", "system_status"]`
|
||||||
|
|
||||||
|
### Process Management
|
||||||
|
```bash
|
||||||
|
# Kill existing relay processes
|
||||||
|
pkill -f "c_relay_"
|
||||||
|
|
||||||
|
# Check running processes
|
||||||
|
ps aux | grep c_relay_
|
||||||
|
|
||||||
|
# Force kill port binding
|
||||||
|
fuser -k 8888/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Compilation Specifics
|
||||||
|
- ARM64 requires explicit dependency installation: `make install-arm64-deps`
|
||||||
|
- Uses `aarch64-linux-gnu-gcc` with specific library paths
|
||||||
|
- PKG_CONFIG_PATH must be set for ARM64: `/usr/lib/aarch64-linux-gnu/pkgconfig`
|
||||||
|
|
||||||
|
### Testing Integration
|
||||||
|
- Tests expect relay running on default port
|
||||||
|
- Use `tests/quick_error_tests.sh` for validation
|
||||||
|
- Event configuration tests: `tests/event_config_tests.sh`
|
||||||
|
|
||||||
|
### SystemD Integration Considerations
|
||||||
|
- Service runs as `c-relay` user in `/opt/c-relay`
|
||||||
|
- Database files created in WorkingDirectory automatically
|
||||||
|
- No environment variables needed (event-based config)
|
||||||
|
- Resource limits: 65536 file descriptors, 4096 processes
|
||||||
|
|
||||||
|
### Development vs Production Differences
|
||||||
|
- Development: `make_and_restart_relay.sh` (default database cleanup)
|
||||||
|
- Production: `make_and_restart_relay.sh --preserve-database`
|
||||||
|
- Debug build requires manual gdb attachment to architecture-specific binary
|
||||||
|
|
||||||
|
### Critical File Dependencies
|
||||||
|
- `nostr_core_lib/` submodule must be initialized and built first
|
||||||
|
- Version header auto-generated from git tags: `src/version.h`
|
||||||
|
- Schema embedded in binary from `src/sql_schema.h`
|
||||||
|
|
||||||
|
### WebSocket Protocol Specifics
|
||||||
|
- Supports both WebSocket (Nostr protocol) and HTTP (NIP-11)
|
||||||
|
- NIP-11 requires `Accept: application/nostr+json` header
|
||||||
|
- CORS headers automatically added for NIP-11 compliance
|
||||||
|
|
||||||
|
### Memory Management Notes
|
||||||
|
- Persistent subscription system with thread-safe global manager
|
||||||
|
- Per-session subscription limits enforced
|
||||||
|
- Event filtering done at C level, not SQL level for NIP-40 expiration
|
||||||
|
|
||||||
|
### Configuration Override Behavior
|
||||||
|
- CLI port override only affects first-time startup
|
||||||
|
- After database creation, all config comes from events
|
||||||
|
- Database path cannot be changed after initialization
|
||||||
|
|
||||||
|
## Non-Obvious Pitfalls
|
||||||
|
|
||||||
|
1. **Database Lock Issues**: Script handles SQLite locking by killing existing processes first
|
||||||
|
2. **Port Race Conditions**: Pre-check + libwebsockets binding can still fail due to timing
|
||||||
|
3. **Key Loss**: Admin private key loss requires complete database deletion and restart
|
||||||
|
4. **Architecture Detection**: Build system auto-detects but cross-compilation requires manual setup
|
||||||
|
5. **Event Storage**: Ephemeral events (kind 20000-29999) accepted but not stored
|
||||||
|
6. **Signature Validation**: All events validated with `nostr_verify_event_signature()` from nostr_core_lib
|
||||||
|
|
||||||
|
## Quick Debugging Commands
|
||||||
|
```bash
|
||||||
|
# Check relay status
|
||||||
|
ps aux | grep c_relay_ && netstat -tln | grep 8888
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f relay.log
|
||||||
|
|
||||||
|
# Test WebSocket connection
|
||||||
|
wscat -c ws://localhost:8888
|
||||||
|
|
||||||
|
# Test NIP-11 endpoint
|
||||||
|
curl -H "Accept: application/nostr+json" http://localhost:8888
|
||||||
|
|
||||||
|
# Find database files
|
||||||
|
find . -name "*.db" -type f
|
||||||
139
Dockerfile.alpine-musl
Normal file
139
Dockerfile.alpine-musl
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Alpine-based MUSL static binary builder for C-Relay
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
ARG DEBUG_BUILD=false
|
||||||
|
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
pkgconfig \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static \
|
||||||
|
curl-dev \
|
||||||
|
curl-static \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-static \
|
||||||
|
linux-headers \
|
||||||
|
wget \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build libsecp256k1 static (cached layer - only rebuilds if Alpine version changes)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr \
|
||||||
|
CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/secp256k1
|
||||||
|
|
||||||
|
# Build libwebsockets static with minimal features (cached layer)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone --depth 1 --branch v4.3.3 https://github.com/warmcat/libwebsockets.git && \
|
||||||
|
cd libwebsockets && \
|
||||||
|
mkdir build && cd build && \
|
||||||
|
cmake .. \
|
||||||
|
-DLWS_WITH_STATIC=ON \
|
||||||
|
-DLWS_WITH_SHARED=OFF \
|
||||||
|
-DLWS_WITH_SSL=ON \
|
||||||
|
-DLWS_WITHOUT_TESTAPPS=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_SERVER=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_CLIENT=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_PING=ON \
|
||||||
|
-DLWS_WITH_HTTP2=OFF \
|
||||||
|
-DLWS_WITH_LIBUV=OFF \
|
||||||
|
-DLWS_WITH_LIBEVENT=OFF \
|
||||||
|
-DLWS_IPV6=ON \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||||
|
-DCMAKE_C_FLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/libwebsockets
|
||||||
|
|
||||||
|
# Copy only submodule configuration and git directory
|
||||||
|
COPY .gitmodules /build/.gitmodules
|
||||||
|
COPY .git /build/.git
|
||||||
|
|
||||||
|
# Clean up any stale submodule references (nips directory is not a submodule)
|
||||||
|
RUN git rm --cached nips 2>/dev/null || true
|
||||||
|
|
||||||
|
# Initialize submodules (cached unless .gitmodules changes)
|
||||||
|
RUN git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
|
||||||
|
COPY nostr_core_lib /build/nostr_core_lib/
|
||||||
|
|
||||||
|
# Copy c_utils_lib source files (cached unless c_utils_lib changes)
|
||||||
|
COPY c_utils_lib /build/c_utils_lib/
|
||||||
|
|
||||||
|
# Build c_utils_lib with MUSL-compatible flags (cached unless c_utils_lib changes)
|
||||||
|
RUN cd c_utils_lib && \
|
||||||
|
sed -i 's/CFLAGS = -Wall -Wextra -std=c99 -O2 -g/CFLAGS = -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -O2 -g/' Makefile && \
|
||||||
|
make clean && \
|
||||||
|
make
|
||||||
|
|
||||||
|
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
|
||||||
|
# Disable fortification in build.sh to prevent __*_chk symbol issues
|
||||||
|
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 044(Encryption), 059(Gift Wrap - required by NIP-17)
|
||||||
|
RUN cd nostr_core_lib && \
|
||||||
|
chmod +x build.sh && \
|
||||||
|
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||||
|
rm -f *.o *.a 2>/dev/null || true && \
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59
|
||||||
|
|
||||||
|
# Copy c-relay source files LAST (only this layer rebuilds on source changes)
|
||||||
|
COPY src/ /build/src/
|
||||||
|
COPY Makefile /build/Makefile
|
||||||
|
|
||||||
|
# Build c-relay with full static linking (only rebuilds when src/ changes)
|
||||||
|
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||||
|
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||||
|
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||||
|
CFLAGS="-g -O0 -DDEBUG"; \
|
||||||
|
STRIP_CMD=""; \
|
||||||
|
echo "Building with DEBUG symbols enabled"; \
|
||||||
|
else \
|
||||||
|
CFLAGS="-O2"; \
|
||||||
|
STRIP_CMD="strip /build/c_relay_static"; \
|
||||||
|
echo "Building optimized production binary"; \
|
||||||
|
fi && \
|
||||||
|
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||||
|
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||||
|
src/main.c src/config.c src/dm_admin.c src/request_validator.c \
|
||||||
|
src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c \
|
||||||
|
src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c \
|
||||||
|
-o /build/c_relay_static \
|
||||||
|
c_utils_lib/libc_utils.a \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||||
|
-lcurl -lz -lpthread -lm -ldl && \
|
||||||
|
eval "$STRIP_CMD"
|
||||||
|
|
||||||
|
# Verify it's truly static
|
||||||
|
RUN echo "=== Binary Information ===" && \
|
||||||
|
file /build/c_relay_static && \
|
||||||
|
ls -lh /build/c_relay_static && \
|
||||||
|
echo "=== Checking for dynamic dependencies ===" && \
|
||||||
|
(ldd /build/c_relay_static 2>&1 || echo "Binary is static") && \
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
|
||||||
|
# Output stage - just the binary
|
||||||
|
FROM scratch AS output
|
||||||
|
COPY --from=builder /build/c_relay_static /c_relay_static
|
||||||
107
Makefile
107
Makefile
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -std=c99 -g -O2
|
CFLAGS = -Wall -Wextra -std=c99 -g -O2
|
||||||
INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
|
INCLUDES = -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
|
||||||
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl
|
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -Lc_utils_lib -lc_utils
|
||||||
|
|
||||||
# Build directory
|
# Build directory
|
||||||
BUILD_DIR = build
|
BUILD_DIR = build
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
MAIN_SRC = src/main.c src/config.c
|
MAIN_SRC = src/main.c src/config.c src/dm_admin.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c
|
||||||
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||||
|
C_UTILS_LIB = c_utils_lib/libc_utils.a
|
||||||
|
|
||||||
# Architecture detection
|
# Architecture detection
|
||||||
ARCH = $(shell uname -m)
|
ARCH = $(shell uname -m)
|
||||||
@@ -32,14 +33,27 @@ $(BUILD_DIR):
|
|||||||
mkdir -p $(BUILD_DIR)
|
mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
# Check if nostr_core_lib is built
|
# Check if nostr_core_lib is built
|
||||||
|
# Explicitly specify NIPs to ensure NIP-44 (encryption) is included
|
||||||
|
# NIPs: 1 (basic), 6 (keys), 13 (PoW), 17 (DMs), 19 (bech32), 44 (encryption), 59 (gift wrap)
|
||||||
$(NOSTR_CORE_LIB):
|
$(NOSTR_CORE_LIB):
|
||||||
@echo "Building nostr_core_lib..."
|
@echo "Building nostr_core_lib with required NIPs (including NIP-44 for encryption)..."
|
||||||
cd nostr_core_lib && ./build.sh
|
cd nostr_core_lib && ./build.sh --nips=1,6,13,17,19,44,59
|
||||||
|
|
||||||
# Generate version.h from git tags
|
# Check if c_utils_lib is built
|
||||||
src/version.h:
|
$(C_UTILS_LIB):
|
||||||
@if [ -d .git ]; then \
|
@echo "Building c_utils_lib..."
|
||||||
echo "Generating version.h from git tags..."; \
|
cd c_utils_lib && ./build.sh lib
|
||||||
|
|
||||||
|
# Update main.h version information (requires main.h to exist)
|
||||||
|
src/main.h:
|
||||||
|
@if [ ! -f src/main.h ]; then \
|
||||||
|
echo "ERROR: src/main.h not found!"; \
|
||||||
|
echo "Please ensure src/main.h exists with relay metadata."; \
|
||||||
|
echo "Copy from a backup or create manually with proper relay configuration."; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
if [ -d .git ]; then \
|
||||||
|
echo "Updating main.h version information from git tags..."; \
|
||||||
RAW_VERSION=$$(git describe --tags --always 2>/dev/null || echo "unknown"); \
|
RAW_VERSION=$$(git describe --tags --always 2>/dev/null || echo "unknown"); \
|
||||||
if echo "$$RAW_VERSION" | grep -q "^v[0-9]"; then \
|
if echo "$$RAW_VERSION" | grep -q "^v[0-9]"; then \
|
||||||
CLEAN_VERSION=$$(echo "$$RAW_VERSION" | sed 's/^v//' | cut -d- -f1); \
|
CLEAN_VERSION=$$(echo "$$RAW_VERSION" | sed 's/^v//' | cut -d- -f1); \
|
||||||
@@ -51,54 +65,34 @@ src/version.h:
|
|||||||
VERSION="v0.0.0"; \
|
VERSION="v0.0.0"; \
|
||||||
MAJOR=0; MINOR=0; PATCH=0; \
|
MAJOR=0; MINOR=0; PATCH=0; \
|
||||||
fi; \
|
fi; \
|
||||||
echo "/* Auto-generated version information */" > src/version.h; \
|
echo "Updating version information in existing main.h..."; \
|
||||||
echo "#ifndef VERSION_H" >> src/version.h; \
|
sed -i "s/#define VERSION \".*\"/#define VERSION \"$$VERSION\"/g" src/main.h; \
|
||||||
echo "#define VERSION_H" >> src/version.h; \
|
sed -i "s/#define VERSION_MAJOR [0-9]*/#define VERSION_MAJOR $$MAJOR/g" src/main.h; \
|
||||||
echo "" >> src/version.h; \
|
sed -i "s/#define VERSION_MINOR [0-9]*/#define VERSION_MINOR $$MINOR/g" src/main.h; \
|
||||||
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
|
sed -i "s/#define VERSION_PATCH [0-9]*/#define VERSION_PATCH $$PATCH/g" src/main.h; \
|
||||||
echo "#define VERSION_MAJOR $$MAJOR" >> src/version.h; \
|
echo "Updated main.h version to: $$VERSION"; \
|
||||||
echo "#define VERSION_MINOR $$MINOR" >> src/version.h; \
|
|
||||||
echo "#define VERSION_PATCH $$PATCH" >> src/version.h; \
|
|
||||||
echo "" >> src/version.h; \
|
|
||||||
echo "#endif /* VERSION_H */" >> src/version.h; \
|
|
||||||
echo "Generated version.h with clean version: $$VERSION"; \
|
|
||||||
elif [ ! -f src/version.h ]; then \
|
|
||||||
echo "Git not available and version.h missing, creating fallback version.h..."; \
|
|
||||||
VERSION="v0.0.0"; \
|
|
||||||
echo "/* Auto-generated version information */" > src/version.h; \
|
|
||||||
echo "#ifndef VERSION_H" >> src/version.h; \
|
|
||||||
echo "#define VERSION_H" >> src/version.h; \
|
|
||||||
echo "" >> src/version.h; \
|
|
||||||
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
|
|
||||||
echo "#define VERSION_MAJOR 0" >> src/version.h; \
|
|
||||||
echo "#define VERSION_MINOR 0" >> src/version.h; \
|
|
||||||
echo "#define VERSION_PATCH 0" >> src/version.h; \
|
|
||||||
echo "" >> src/version.h; \
|
|
||||||
echo "#endif /* VERSION_H */" >> src/version.h; \
|
|
||||||
echo "Created fallback version.h with version: $$VERSION"; \
|
|
||||||
else \
|
else \
|
||||||
echo "Git not available, preserving existing version.h"; \
|
echo "Git not available, preserving existing main.h version information"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Force version.h regeneration (useful for development)
|
# Update main.h version information (requires existing main.h)
|
||||||
force-version:
|
force-version:
|
||||||
@echo "Force regenerating version.h..."
|
@echo "Force updating main.h version information..."
|
||||||
@rm -f src/version.h
|
@$(MAKE) src/main.h
|
||||||
@$(MAKE) src/version.h
|
|
||||||
|
|
||||||
# Build the relay
|
# Build the relay
|
||||||
$(TARGET): $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||||
@echo "Compiling C-Relay for architecture: $(ARCH)"
|
@echo "Compiling C-Relay for architecture: $(ARCH)"
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
|
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
|
||||||
@echo "Build complete: $(TARGET)"
|
@echo "Build complete: $(TARGET)"
|
||||||
|
|
||||||
# Build for specific architectures
|
# Build for specific architectures
|
||||||
x86: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||||
@echo "Building C-Relay for x86_64..."
|
@echo "Building C-Relay for x86_64..."
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
|
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
|
||||||
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
|
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
|
||||||
|
|
||||||
arm64: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
|
||||||
@echo "Cross-compiling C-Relay for ARM64..."
|
@echo "Cross-compiling C-Relay for ARM64..."
|
||||||
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||||
echo "ERROR: ARM64 cross-compiler not found."; \
|
echo "ERROR: ARM64 cross-compiler not found."; \
|
||||||
@@ -122,7 +116,7 @@ arm64: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
|||||||
fi
|
fi
|
||||||
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
|
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
|
||||||
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
|
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
|
||||||
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) \
|
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
|
||||||
-L/usr/lib/aarch64-linux-gnu $(LIBS)
|
-L/usr/lib/aarch64-linux-gnu $(LIBS)
|
||||||
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
|
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
|
||||||
|
|
||||||
@@ -171,12 +165,12 @@ init-db:
|
|||||||
# Clean build artifacts
|
# Clean build artifacts
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
rm -f src/version.h
|
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
# Clean everything including nostr_core_lib
|
# Clean everything including nostr_core_lib and c_utils_lib
|
||||||
clean-all: clean
|
clean-all: clean
|
||||||
cd nostr_core_lib && make clean 2>/dev/null || true
|
cd nostr_core_lib && make clean 2>/dev/null || true
|
||||||
|
cd c_utils_lib && make clean 2>/dev/null || true
|
||||||
|
|
||||||
# Install dependencies (Ubuntu/Debian)
|
# Install dependencies (Ubuntu/Debian)
|
||||||
install-deps:
|
install-deps:
|
||||||
@@ -210,6 +204,23 @@ help:
|
|||||||
@echo " make check-toolchain # Check what compilers are available"
|
@echo " make check-toolchain # Check what compilers are available"
|
||||||
@echo " make test # Run tests"
|
@echo " make test # Run tests"
|
||||||
@echo " make init-db # Set up database"
|
@echo " make init-db # Set up database"
|
||||||
@echo " make force-version # Force regenerate version.h from git"
|
@echo " make force-version # Force regenerate main.h from git"
|
||||||
|
|
||||||
|
# Build fully static MUSL binaries using Docker
|
||||||
|
static-musl-x86_64:
|
||||||
|
@echo "Building fully static MUSL binary for x86_64..."
|
||||||
|
docker buildx build --platform linux/amd64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-x86_64 --load .
|
||||||
|
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-x86_64 sh -c "cp /c_relay_static_musl_x86_64 /output/"
|
||||||
|
@echo "Static binary created: build/c_relay_static_musl_x86_64"
|
||||||
|
|
||||||
|
static-musl-arm64:
|
||||||
|
@echo "Building fully static MUSL binary for ARM64..."
|
||||||
|
docker buildx build --platform linux/arm64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-arm64 --load .
|
||||||
|
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-arm64 sh -c "cp /c_relay_static_musl_x86_64 /output/c_relay_static_musl_arm64"
|
||||||
|
@echo "Static binary created: build/c_relay_static_musl_arm64"
|
||||||
|
|
||||||
|
static-musl: static-musl-x86_64 static-musl-arm64
|
||||||
|
@echo "Built static MUSL binaries for both architectures"
|
||||||
|
|
||||||
|
.PHONY: static-musl-x86_64 static-musl-arm64 static-musl
|
||||||
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version
|
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version
|
||||||
622
README.md
622
README.md
@@ -1,267 +1,8 @@
|
|||||||
# C Nostr Relay - Event-Based Configuration System
|
# C-Nostr Relay
|
||||||
|
|
||||||
A high-performance Nostr relay implemented in C with SQLite backend, featuring a revolutionary **zero-configuration** approach using event-based configuration management.
|
A high-performance Nostr relay implemented in C with SQLite backend, featuring nostr event-based management.
|
||||||
|
|
||||||
## 🌟 Key Features
|
## Supported NIPs
|
||||||
|
|
||||||
- **🔧 Zero Configuration**: No config files or command line arguments needed
|
|
||||||
- **🔑 Event-Based Config**: All settings stored as kind 33334 Nostr events
|
|
||||||
- **🚀 Real-Time Updates**: Configuration changes applied instantly via WebSocket
|
|
||||||
- **🛡️ Cryptographic Security**: Configuration events cryptographically signed and validated
|
|
||||||
- **📊 SQLite Backend**: High-performance event storage with optimized schema
|
|
||||||
- **🔄 Auto Key Generation**: Secure admin and relay keypairs generated on first startup
|
|
||||||
- **💾 Database Per Relay**: Each relay instance uses `<relay_pubkey>.nrdb` database naming
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### 1. Build the Relay
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd c-relay
|
|
||||||
git submodule update --init --recursive
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Start the Relay
|
|
||||||
```bash
|
|
||||||
./build/c_relay_x86
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** No configuration files, no command line arguments needed.
|
|
||||||
|
|
||||||
### 3. Save Your Admin Keys (IMPORTANT!)
|
|
||||||
On first startup, the relay will display:
|
|
||||||
|
|
||||||
```
|
|
||||||
=================================================================
|
|
||||||
IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!
|
|
||||||
=================================================================
|
|
||||||
Admin Private Key: f8491814ea288260dad2ab52c09b3b037e75e83e8b24feb9bdc328423922be44
|
|
||||||
Admin Public Key: 07fc2cdd8bdc0c60eefcc9e37e67fef88206bc84fadb894c283b006554ac687b
|
|
||||||
|
|
||||||
Relay Private Key: a1b2c3d4e5f6...
|
|
||||||
Relay Public Key: 1a2b3c4d5e6f...
|
|
||||||
|
|
||||||
Database: dc9a93fd0ffba7041f6df0602e5021913a42fcaf6dbf40f43ecdc011177b4d94.nrdb
|
|
||||||
=================================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Save the admin private key securely** - it's needed to update relay configuration and is only displayed once!
|
|
||||||
|
|
||||||
## 📋 System Requirements
|
|
||||||
|
|
||||||
- **OS**: Linux, macOS, or Windows (WSL)
|
|
||||||
- **Dependencies**:
|
|
||||||
- SQLite 3
|
|
||||||
- libwebsockets
|
|
||||||
- OpenSSL/LibreSSL
|
|
||||||
- libsecp256k1
|
|
||||||
- libcurl
|
|
||||||
- zlib
|
|
||||||
|
|
||||||
## 🏗️ Event-Based Configuration System
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
Traditional Nostr relays require configuration files, environment variables, or command line arguments. This relay uses a **revolutionary approach**:
|
|
||||||
|
|
||||||
1. **First-Time Startup**: Generates cryptographically secure admin and relay keypairs
|
|
||||||
2. **Database Creation**: Creates `<relay_pubkey>.nrdb` database file
|
|
||||||
3. **Default Configuration**: Creates initial kind 33334 configuration event with sensible defaults
|
|
||||||
4. **Real-Time Updates**: Administrators send new kind 33334 events to update configuration
|
|
||||||
5. **Instant Application**: Changes are applied immediately without restart
|
|
||||||
|
|
||||||
### Configuration Updates
|
|
||||||
|
|
||||||
To update relay configuration, send a signed kind 33334 event:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kind": 33334,
|
|
||||||
"content": "C Nostr Relay Configuration",
|
|
||||||
"tags": [
|
|
||||||
["d", "<relay_pubkey>"],
|
|
||||||
["relay_description", "My awesome Nostr relay"],
|
|
||||||
["max_subscriptions_per_client", "25"],
|
|
||||||
["pow_min_difficulty", "16"],
|
|
||||||
["nip40_expiration_enabled", "true"]
|
|
||||||
],
|
|
||||||
"created_at": 1234567890,
|
|
||||||
"pubkey": "<admin_pubkey>",
|
|
||||||
"id": "...",
|
|
||||||
"sig": "..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Send this event to your relay via WebSocket, and changes are applied instantly.
|
|
||||||
|
|
||||||
### Configurable Parameters
|
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
|
||||||
|-----------|-------------|---------|
|
|
||||||
| `relay_description` | Relay description (NIP-11) | "C Nostr Relay" |
|
|
||||||
| `relay_contact` | Admin contact info | "" |
|
|
||||||
| `max_subscriptions_per_client` | Max subscriptions per client | "25" |
|
|
||||||
| `max_total_subscriptions` | Total subscription limit | "5000" |
|
|
||||||
| `pow_min_difficulty` | NIP-13 PoW difficulty | "0" |
|
|
||||||
| `pow_mode` | PoW validation mode | "optional" |
|
|
||||||
| `nip40_expiration_enabled` | Enable NIP-40 expiration | "true" |
|
|
||||||
| `nip40_expiration_strict` | Strict expiration mode | "false" |
|
|
||||||
| `max_message_length` | Max message size | "65536" |
|
|
||||||
| `max_event_tags` | Max tags per event | "2000" |
|
|
||||||
| `max_content_length` | Max content length | "65536" |
|
|
||||||
|
|
||||||
## 🔧 Deployment
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
```bash
|
|
||||||
# Build the relay
|
|
||||||
make
|
|
||||||
|
|
||||||
# Run directly
|
|
||||||
./build/c_relay_x86
|
|
||||||
```
|
|
||||||
|
|
||||||
### SystemD Service (Recommended)
|
|
||||||
```bash
|
|
||||||
# Install as system service
|
|
||||||
sudo systemd/install-service.sh
|
|
||||||
|
|
||||||
# Start the service
|
|
||||||
sudo systemctl start c-relay
|
|
||||||
|
|
||||||
# Enable auto-start on boot
|
|
||||||
sudo systemctl enable c-relay
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
sudo journalctl -u c-relay -f
|
|
||||||
```
|
|
||||||
|
|
||||||
See [`systemd/README.md`](systemd/README.md) for detailed deployment documentation.
|
|
||||||
|
|
||||||
### Docker (Coming Soon)
|
|
||||||
Docker support is planned for future releases.
|
|
||||||
|
|
||||||
## 📊 Database Schema
|
|
||||||
|
|
||||||
The relay uses an optimized SQLite schema (version 4) with these key features:
|
|
||||||
|
|
||||||
- **Event-based storage**: All Nostr events in single `events` table
|
|
||||||
- **JSON tags support**: Native JSON storage for event tags
|
|
||||||
- **Performance optimized**: Multiple indexes for fast queries
|
|
||||||
- **Subscription logging**: Optional detailed subscription analytics
|
|
||||||
- **Auto-cleanup**: Automatic ephemeral event cleanup
|
|
||||||
- **Replaceable events**: Proper handling of replaceable/addressable events
|
|
||||||
|
|
||||||
## 🛡️ Security Features
|
|
||||||
|
|
||||||
- **Cryptographic validation**: All configuration events cryptographically verified
|
|
||||||
- **Admin-only config**: Only authorized admin pubkey can update configuration
|
|
||||||
- **Signature verification**: Uses `nostr_verify_event_signature()` for validation
|
|
||||||
- **Event structure validation**: Complete event structure validation
|
|
||||||
- **Secure key generation**: Uses `/dev/urandom` for cryptographically secure keys
|
|
||||||
- **No secrets storage**: Admin private key never stored on disk
|
|
||||||
|
|
||||||
## 🔌 Network Configuration
|
|
||||||
|
|
||||||
- **Default Port**: 8888 (WebSocket)
|
|
||||||
- **Protocol**: WebSocket with Nostr message format
|
|
||||||
- **Endpoints**:
|
|
||||||
- `ws://localhost:8888` - WebSocket relay
|
|
||||||
- `http://localhost:8888` - NIP-11 relay information (HTTP GET)
|
|
||||||
|
|
||||||
## 🏃♂️ Usage Examples
|
|
||||||
|
|
||||||
### Connect with a Nostr Client
|
|
||||||
```javascript
|
|
||||||
const relay = new WebSocket('ws://localhost:8888');
|
|
||||||
relay.send(JSON.stringify(["REQ", "sub1", {"kinds": [1], "limit": 10}]));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Configuration (using `nostrtool` or similar)
|
|
||||||
```bash
|
|
||||||
# Create configuration event with nostrtool
|
|
||||||
nostrtool event --kind 33334 --content "Updated config" \
|
|
||||||
--tag d <relay_pubkey> \
|
|
||||||
--tag relay_description "My updated relay" \
|
|
||||||
--private-key <admin_private_key>
|
|
||||||
|
|
||||||
# Send to relay
|
|
||||||
nostrtool send ws://localhost:8888 <event_json>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Monitoring and Analytics
|
|
||||||
|
|
||||||
### View Relay Status
|
|
||||||
```bash
|
|
||||||
# Check if relay is running
|
|
||||||
ps aux | grep c_relay
|
|
||||||
|
|
||||||
# Check network port
|
|
||||||
netstat -tln | grep 8888
|
|
||||||
|
|
||||||
# View recent logs
|
|
||||||
tail -f relay.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Analytics
|
|
||||||
```bash
|
|
||||||
# Connect to relay database
|
|
||||||
sqlite3 <relay_pubkey>.nrdb
|
|
||||||
|
|
||||||
# View relay statistics
|
|
||||||
SELECT * FROM event_stats;
|
|
||||||
|
|
||||||
# View configuration events
|
|
||||||
SELECT * FROM configuration_events;
|
|
||||||
|
|
||||||
# View recent events
|
|
||||||
SELECT * FROM recent_events LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
### Run Error Handling Tests
|
|
||||||
```bash
|
|
||||||
# Comprehensive test suite
|
|
||||||
tests/event_config_tests.sh
|
|
||||||
|
|
||||||
# Quick validation tests
|
|
||||||
tests/quick_error_tests.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
```bash
|
|
||||||
# Test WebSocket connection
|
|
||||||
wscat -c ws://localhost:8888
|
|
||||||
|
|
||||||
# Test NIP-11 information
|
|
||||||
curl http://localhost:8888
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development
|
|
||||||
|
|
||||||
### Build from Source
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd c-relay
|
|
||||||
git submodule update --init --recursive
|
|
||||||
make clean && make
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Build
|
|
||||||
```bash
|
|
||||||
make debug
|
|
||||||
gdb ./build/c_relay_x86
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Make changes with tests
|
|
||||||
4. Submit a pull request
|
|
||||||
|
|
||||||
## 📜 Supported NIPs
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
NOTE FOR ASSISTANTS: When updating the NIPs checklist below, ONLY change [ ] to [x] to mark as complete.
|
NOTE FOR ASSISTANTS: When updating the NIPs checklist below, ONLY change [ ] to [x] to mark as complete.
|
||||||
@@ -276,68 +17,331 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
|
|||||||
- [x] NIP-20: Command Results
|
- [x] NIP-20: Command Results
|
||||||
- [x] NIP-33: Parameterized Replaceable Events
|
- [x] NIP-33: Parameterized Replaceable Events
|
||||||
- [x] NIP-40: Expiration Timestamp
|
- [x] NIP-40: Expiration Timestamp
|
||||||
- [ ] NIP-42: Authentication of clients to relays
|
- [x] NIP-42: Authentication of clients to relays
|
||||||
- [ ] NIP-45: Counting results
|
- [x] NIP-45: Counting results
|
||||||
- [ ] NIP-50: Keywords filter
|
- [x] NIP-50: Keywords filter
|
||||||
- [ ] NIP-70: Protected Events
|
- [x] NIP-70: Protected Events
|
||||||
|
|
||||||
## 🆘 Troubleshooting
|
## Quick Start
|
||||||
|
|
||||||
### Common Issues
|
Get your C-Relay up and running in minutes with a static binary (no dependencies required):
|
||||||
|
|
||||||
|
### 1. Download Static Binary
|
||||||
|
|
||||||
|
Download the latest static release from the [releases page](https://git.laantungir.net/laantungir/c-relay/releases):
|
||||||
|
|
||||||
**Relay won't start**
|
|
||||||
```bash
|
```bash
|
||||||
# Check for port conflicts
|
# Static binary - works on all Linux distributions (no dependencies)
|
||||||
netstat -tln | grep 8888
|
wget https://git.laantungir.net/laantungir/c-relay/releases/download/v0.6.0/c-relay-v0.6.0-linux-x86_64-static
|
||||||
|
chmod +x c-relay-v0.6.0-linux-x86_64-static
|
||||||
# Check permissions
|
mv c-relay-v0.6.0-linux-x86_64-static c-relay
|
||||||
ls -la build/c_relay_x86
|
|
||||||
|
|
||||||
# Check dependencies
|
|
||||||
ldd build/c_relay_x86
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Lost admin private key**
|
### 2. Start the Relay
|
||||||
- If you lose the admin private key, you cannot update configuration
|
|
||||||
- You must delete the database file and restart (loses all events)
|
Simply run the binary - no configuration files needed:
|
||||||
- The relay will generate new keys on first startup
|
|
||||||
|
|
||||||
**Database corruption**
|
|
||||||
```bash
|
```bash
|
||||||
# Check database integrity
|
./c-relay
|
||||||
sqlite3 <relay_pubkey>.nrdb "PRAGMA integrity_check;"
|
|
||||||
|
|
||||||
# If corrupted, remove database (loses all events)
|
|
||||||
rm <relay_pubkey>.nrdb*
|
|
||||||
./build/c_relay_x86 # Will create fresh database
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Configuration not updating**
|
On first startup, you'll see:
|
||||||
- Ensure configuration events are properly signed
|
- **Admin Private Key**: Save this securely! You'll need it for administration
|
||||||
- Check that admin pubkey matches the one from first startup
|
- **Relay Public Key**: Your relay's identity on the Nostr network
|
||||||
- Verify WebSocket connection is active
|
- **Port Information**: Default is 8888, or the next available port
|
||||||
- Check relay logs for validation errors
|
|
||||||
|
|
||||||
## 📄 License
|
### 3. Access the Web Interface
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
Open your browser and navigate to:
|
||||||
|
```
|
||||||
|
http://localhost:8888/api/
|
||||||
|
```
|
||||||
|
|
||||||
## 🤝 Support
|
The web interface provides:
|
||||||
|
- Real-time configuration management
|
||||||
|
- Database statistics dashboard
|
||||||
|
- Auth rules management
|
||||||
|
- Secure admin authentication with your Nostr identity
|
||||||
|
|
||||||
- **Issues**: Report bugs and feature requests on GitHub
|
### 4. Test Your Relay
|
||||||
- **Documentation**: See `docs/` directory for technical details
|
|
||||||
- **Deployment**: See `systemd/README.md` for production deployment
|
|
||||||
- **Community**: Join the Nostr development community
|
|
||||||
|
|
||||||
## 🚀 Future Roadmap
|
Test basic connectivity:
|
||||||
|
```bash
|
||||||
|
# Test WebSocket connection
|
||||||
|
curl -H "Accept: application/nostr+json" http://localhost:8888
|
||||||
|
|
||||||
- [ ] Docker containerization
|
# Test with a Nostr client
|
||||||
- [ ] NIP-42 authentication support
|
# Add ws://localhost:8888 to your client's relay list
|
||||||
- [ ] Advanced analytics dashboard
|
```
|
||||||
- [ ] Clustering support for high availability
|
|
||||||
- [ ] Performance optimizations
|
### 5. Configure Your Relay (Optional)
|
||||||
- [ ] Additional NIP implementations
|
|
||||||
|
Use the web interface or send admin commands to customize:
|
||||||
|
- Relay name and description
|
||||||
|
- Authentication rules (whitelist/blacklist)
|
||||||
|
- Connection limits
|
||||||
|
- Proof-of-work requirements
|
||||||
|
|
||||||
|
**That's it!** Your relay is now running with zero configuration required. The event-based configuration system means you can adjust all settings through the web interface or admin API without editing config files.
|
||||||
|
|
||||||
|
|
||||||
|
## Web Admin Interface
|
||||||
|
|
||||||
|
C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides:
|
||||||
|
|
||||||
|
- **Real-time Configuration Management**: View and edit all relay settings through a web UI
|
||||||
|
- **Database Statistics Dashboard**: Monitor event counts, storage usage, and performance metrics
|
||||||
|
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
|
||||||
|
- **NIP-42 Authentication**: Secure access using your Nostr identity
|
||||||
|
- **Event-Based Updates**: All changes are applied as cryptographically signed Nostr events
|
||||||
|
|
||||||
|
The web interface serves embedded static files with no external dependencies and includes proper CORS headers for browser compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
## Administrator API
|
||||||
|
|
||||||
|
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations.
|
||||||
|
|
||||||
|
### Event Structure
|
||||||
|
|
||||||
|
All admin commands use the same unified event structure with NIP-44 encrypted content:
|
||||||
|
|
||||||
|
**Admin Command Event:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "event_id",
|
||||||
|
"pubkey": "admin_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
|
||||||
|
"tags": [
|
||||||
|
["p", "relay_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "event_signature"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `content` field contains a NIP-44 encrypted JSON array representing the command.
|
||||||
|
|
||||||
|
**Admin Response Event:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `content` field contains a NIP-44 encrypted JSON response object.
|
||||||
|
|
||||||
|
### Admin Commands
|
||||||
|
|
||||||
|
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
|
||||||
|
|
||||||
|
| Command Type | Command Format | Description |
|
||||||
|
|--------------|----------------|-------------|
|
||||||
|
| **Configuration Management** |
|
||||||
|
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
|
||||||
|
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
|
||||||
|
| **Auth Rules Management** |
|
||||||
|
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
|
||||||
|
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
|
||||||
|
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
|
||||||
|
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
|
||||||
|
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
|
||||||
|
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
|
||||||
|
| **System Commands** |
|
||||||
|
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
|
||||||
|
| `system_status` | `["system_command", "system_status"]` | Get system status |
|
||||||
|
| `stats_query` | `["stats_query"]` | Get comprehensive database statistics |
|
||||||
|
|
||||||
|
### Available Configuration Keys
|
||||||
|
|
||||||
|
**Basic Relay Settings:**
|
||||||
|
- `relay_name`: Relay name (displayed in NIP-11)
|
||||||
|
- `relay_description`: Relay description text
|
||||||
|
- `relay_contact`: Contact information
|
||||||
|
- `relay_software`: Software URL
|
||||||
|
- `relay_version`: Software version
|
||||||
|
- `supported_nips`: Comma-separated list of supported NIP numbers (e.g., "1,2,4,9,11,12,13,15,16,20,22,33,40,42")
|
||||||
|
- `language_tags`: Comma-separated list of supported language tags (e.g., "en,es,fr" or "*" for all)
|
||||||
|
- `relay_countries`: Comma-separated list of supported country codes (e.g., "US,CA,MX" or "*" for all)
|
||||||
|
- `posting_policy`: Posting policy URL or text
|
||||||
|
- `payments_url`: Payment URL for premium features
|
||||||
|
- `max_connections`: Maximum concurrent connections
|
||||||
|
- `max_subscriptions_per_client`: Max subscriptions per client
|
||||||
|
- `max_event_tags`: Maximum tags per event
|
||||||
|
- `max_content_length`: Maximum event content length
|
||||||
|
|
||||||
|
**Authentication & Access Control:**
|
||||||
|
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
|
||||||
|
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
|
||||||
|
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
|
||||||
|
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
|
||||||
|
|
||||||
|
**Proof of Work & Validation:**
|
||||||
|
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||||
|
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
|
||||||
|
|
||||||
|
### Dynamic Configuration Updates
|
||||||
|
|
||||||
|
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
|
||||||
|
|
||||||
|
**Dynamic Configuration Parameters (No Restart Required):**
|
||||||
|
- All relay information (NIP-11) settings: `relay_name`, `relay_description`, `relay_contact`, `relay_software`, `relay_version`, `supported_nips`, `language_tags`, `relay_countries`, `posting_policy`, `payments_url`
|
||||||
|
- Authentication settings: `auth_enabled`, `nip42_auth_required`, `nip42_auth_required_kinds`, `nip42_challenge_timeout`
|
||||||
|
- Subscription limits: `max_subscriptions_per_client`, `max_total_subscriptions`
|
||||||
|
- Event validation limits: `max_event_tags`, `max_content_length`, `max_message_length`
|
||||||
|
- Proof of Work settings: `pow_min_difficulty`, `pow_mode`
|
||||||
|
- Event expiration settings: `nip40_expiration_enabled`, `nip40_expiration_strict`, `nip40_expiration_filter`, `nip40_expiration_grace_period`
|
||||||
|
|
||||||
|
**Restart-Required Configuration Parameters:**
|
||||||
|
- Connection settings: `max_connections`, `relay_port`
|
||||||
|
- Database and core system settings
|
||||||
|
|
||||||
|
When updating configuration, the admin API response will indicate whether a restart is required for each parameter. Dynamic updates take effect immediately and are reflected in NIP-11 relay information documents without restart.
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
|
||||||
|
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
|
||||||
|
|
||||||
|
#### Response Examples
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auth Rules Query Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Query Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Update Success Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Update Error Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Statistics Query Response:**
|
||||||
|
```json
|
||||||
|
["EVENT", "temp_sub_id", {
|
||||||
|
"id": "response_event_id",
|
||||||
|
"pubkey": "relay_public_key",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "nip44 encrypted:{\"query_type\": \"stats_query\", \"timestamp\": 1234567890, \"database_size_bytes\": 1048576, \"total_events\": 15432, \"database_created_at\": 1234567800, \"latest_event_at\": 1234567890, \"event_kinds\": [{\"kind\": 1, \"count\": 12000, \"percentage\": 77.8}, {\"kind\": 0, \"count\": 2500, \"percentage\": 16.2}], \"time_stats\": {\"total\": 15432, \"last_24h\": 234, \"last_7d\": 1456, \"last_30d\": 5432}, \"top_pubkeys\": [{\"pubkey\": \"abc123...\", \"event_count\": 1234, \"percentage\": 8.0}, {\"pubkey\": \"def456...\", \"event_count\": 987, \"percentage\": 6.4}]}",
|
||||||
|
"tags": [
|
||||||
|
["p", "admin_public_key"]
|
||||||
|
],
|
||||||
|
"sig": "response_event_signature"
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Direct Messaging Admin System
|
||||||
|
|
||||||
|
In addition to the above admin API, c-relay allows the administrator to direct message the relay to get information or control some settings. As long as the administrator is signed in with any nostr client that allows sending nip-17 direct messages (DMs), they can control the relay.
|
||||||
|
|
||||||
|
The is possible because the relay is a full nostr citizen with it's own private and public key, and it knows the administrator's public key.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Available DM commands**
|
||||||
|
|
||||||
|
The intent is not to be strict in the formatting of the DM. So for example if the relay receives any DM from the administrator with the words "stats" or "statistics" in it, it will respond to the administrator with a reply DM with the current relay statistics.
|
||||||
|
|
||||||
|
- `stats`|`statistics`: Relay statistics
|
||||||
|
- `config`|`configuration`: Relay configuration
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**The C Nostr Relay represents the future of Nostr infrastructure - zero configuration, event-based management, and cryptographically secure administration.**
|
|
||||||
|
|||||||
58
api/embedded.html
Normal file
58
api/embedded.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Embedded NOSTR_LOGIN_LITE</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-container {
|
||||||
|
/* No styling - let embedded modal blend seamlessly */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div id="login-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../lite/nostr.bundle.js"></script>
|
||||||
|
<script src="../lite/nostr-lite.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await window.NOSTR_LOGIN_LITE.init({
|
||||||
|
theme:'default',
|
||||||
|
methods: {
|
||||||
|
extension: true,
|
||||||
|
local: true,
|
||||||
|
seedphrase: true,
|
||||||
|
readonly: true,
|
||||||
|
connect: true,
|
||||||
|
remote: true,
|
||||||
|
otp: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.NOSTR_LOGIN_LITE.embed('#login-container', {
|
||||||
|
seamless: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
824
api/index.css
Normal file
824
api/index.css
Normal file
@@ -0,0 +1,824 @@
|
|||||||
|
:root {
|
||||||
|
/* Core Variables (7) */
|
||||||
|
--primary-color: #000000;
|
||||||
|
--secondary-color: #ffffff;
|
||||||
|
--accent-color: #ff0000;
|
||||||
|
--muted-color: #dddddd;
|
||||||
|
--border-color: var(--muted-color);
|
||||||
|
--font-family: "Courier New", Courier, monospace;
|
||||||
|
--border-radius: 5px;
|
||||||
|
--border-width: 1px;
|
||||||
|
|
||||||
|
/* Floating Tab Variables (8) */
|
||||||
|
--tab-bg-logged-out: #ffffff;
|
||||||
|
--tab-bg-logged-in: #ffffff;
|
||||||
|
--tab-bg-opacity-logged-out: 0.9;
|
||||||
|
--tab-bg-opacity-logged-in: 0.2;
|
||||||
|
--tab-color-logged-out: #000000;
|
||||||
|
--tab-color-logged-in: #ffffff;
|
||||||
|
--tab-border-logged-out: #000000;
|
||||||
|
--tab-border-logged-in: #ff0000;
|
||||||
|
--tab-border-opacity-logged-out: 1.0;
|
||||||
|
--tab-border-opacity-logged-in: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Overrides */
|
||||||
|
body.dark-mode {
|
||||||
|
--primary-color: #ffffff;
|
||||||
|
--secondary-color: #000000;
|
||||||
|
--accent-color: #ff0000;
|
||||||
|
--muted-color: #222222;
|
||||||
|
--border-color: var(--muted-color);
|
||||||
|
|
||||||
|
|
||||||
|
--tab-bg-logged-out: #000000;
|
||||||
|
--tab-color-logged-out: #ffffff;
|
||||||
|
--tab-border-logged-out: #ffffff;
|
||||||
|
--tab-bg-logged-in: #000000;
|
||||||
|
--tab-color-logged-in: #ffffff;
|
||||||
|
--tab-border-logged-in: #00ffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
/* line-height: 1.4; */
|
||||||
|
padding: 0;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.main-header {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
|
||||||
|
padding: 15px 20px;
|
||||||
|
z-index: 100;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-info {
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-pubkey-container {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-pubkey-container:hover {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-pubkey-container.copied {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
animation: flash-accent 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-pubkey {
|
||||||
|
font-size: 8px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: pre-line;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-accent {
|
||||||
|
0% { border-color: var(--accent-color); }
|
||||||
|
50% { border-color: var(--accent-color); }
|
||||||
|
100% { border-color: transparent; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-description {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bolder;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-letter {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.05s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-letter.underlined::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-area:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user-image {
|
||||||
|
width: 48px; /* 50% larger than 32px */
|
||||||
|
height: 48px; /* 50% larger than 32px */
|
||||||
|
border-radius: var(--border-radius); /* Curved corners like other elements */
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid transparent; /* Invisible border */
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.logout-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 120px;
|
||||||
|
z-index: 200;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Modal Styles */
|
||||||
|
.login-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal-content {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: var(--secondary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: var(--muted-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-color: var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash animation for refresh button */
|
||||||
|
@keyframes flash-red {
|
||||||
|
0% { border-color: var(--border-color); }
|
||||||
|
50% { border-color: var(--accent-color); }
|
||||||
|
100% { border-color: var(--border-color); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-red {
|
||||||
|
animation: flash-red 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash animation for updated statistics values */
|
||||||
|
@keyframes flash-value {
|
||||||
|
0% { color: var(--primary-color); }
|
||||||
|
50% { color: var(--accent-color); }
|
||||||
|
100% { color: var(--primary-color); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-value {
|
||||||
|
animation: flash-value 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Npub links styling */
|
||||||
|
.npub-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: normal;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.npub-link:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.connected {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.disconnected {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.authenticated {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.config-table {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table th,
|
||||||
|
.config-table td {
|
||||||
|
border: 0.1px solid var(--muted-color);
|
||||||
|
padding: 4px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table tbody tr:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
height: 24px; /* Base height for tbody rows */
|
||||||
|
line-height: 24px; /* Center text vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table td {
|
||||||
|
height: 16px; /* 50% taller than tbody rows would be */
|
||||||
|
line-height: 16px; /* Center text vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline config value inputs - remove borders and padding to fit seamlessly in table cells */
|
||||||
|
.config-value-input {
|
||||||
|
border: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
min-height: auto;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-input:focus {
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
background: var(--secondary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config actions cell - clickable for saving */
|
||||||
|
.config-actions-cell {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center !important;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 60px;
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 60px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions-cell:hover {
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
background-color: var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-display {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 10px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-panel {
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-bottom: 1px solid var(--muted-color);
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp {
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: 10px 0;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
order: -1; /* Show user details first when logged in */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logout-btn {
|
||||||
|
width: auto;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logout-btn:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logout-btn:active {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logout-btn.logout-state {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logout-btn.logout-state:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-pubkey {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User profile header with image */
|
||||||
|
.user-profile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-image-container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-text-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* Allow text to wrap */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-btn {
|
||||||
|
width: auto;
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 10px;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-left: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-btn:hover::after {
|
||||||
|
content: "countdown";
|
||||||
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-rules-controls {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .status {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
min-width: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth Rule Input Sections Styling */
|
||||||
|
.auth-rule-section {
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-rule-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-left: 4px solid var(--border-color);
|
||||||
|
padding-left: 8px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-rule-section p {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted-color);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border-width) solid var(--muted-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 20px;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-status.success {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background-color: #E8F5E8;
|
||||||
|
color: #2E7D32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-status.error {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background-color: #FFEBEE;
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-status.warning {
|
||||||
|
border-color: #FF9800;
|
||||||
|
background-color: #FFF3E0;
|
||||||
|
color: #E65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
border: var(--border-width) solid #FF9800;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: #FFF3E0;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #E65100;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box strong {
|
||||||
|
color: #D84315;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating tab styles */
|
||||||
|
.floating-tab {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: var(--border-width) solid;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-tab--logged-out {
|
||||||
|
background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-out));
|
||||||
|
color: var(--tab-color-logged-out);
|
||||||
|
border-color: rgba(0, 0, 0, var(--tab-border-opacity-logged-out));
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-tab--logged-in {
|
||||||
|
background: rgba(0, 0, 0, var(--tab-bg-opacity-logged-in));
|
||||||
|
color: var(--tab-color-logged-in);
|
||||||
|
border-color: rgba(255, 0, 0, var(--tab-border-opacity-logged-in));
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
292
api/index.html
Normal file
292
api/index.html
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>C-Relay Admin</title>
|
||||||
|
<link rel="stylesheet" href="/api/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Header with title and profile display -->
|
||||||
|
<div class="section">
|
||||||
|
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-title">
|
||||||
|
<span class="relay-letter" data-letter="R">R</span>
|
||||||
|
<span class="relay-letter" data-letter="E">E</span>
|
||||||
|
<span class="relay-letter" data-letter="L">L</span>
|
||||||
|
<span class="relay-letter" data-letter="A">A</span>
|
||||||
|
<span class="relay-letter" data-letter="Y">Y</span>
|
||||||
|
</div>
|
||||||
|
<div class="relay-info">
|
||||||
|
<div id="relay-name" class="relay-name">C-Relay</div>
|
||||||
|
<div id="relay-description" class="relay-description">Loading...</div>
|
||||||
|
<div id="relay-pubkey-container" class="relay-pubkey-container">
|
||||||
|
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-area" id="profile-area" style="display: none;">
|
||||||
|
<div class="admin-label">admin</div>
|
||||||
|
<div class="profile-container">
|
||||||
|
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
|
||||||
|
<span id="header-user-name" class="header-user-name">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<!-- Logout dropdown -->
|
||||||
|
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
|
||||||
|
<button type="button" id="dark-mode-btn" class="logout-btn">🌙 DARK MODE</button>
|
||||||
|
<button type="button" id="logout-btn" class="logout-btn">LOGOUT</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Modal Overlay -->
|
||||||
|
<div id="login-modal" class="login-modal-overlay" style="display: none;">
|
||||||
|
<div class="login-modal-content">
|
||||||
|
<div id="login-modal-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DATABASE STATISTICS Section -->
|
||||||
|
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>DATABASE STATISTICS</h2>
|
||||||
|
<button type="button" id="refresh-stats-btn" class="countdown-btn"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Database Overview Table -->
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="config-table-container">
|
||||||
|
<table class="config-table" id="stats-overview-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Metric</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="stats-overview-table-body">
|
||||||
|
<tr>
|
||||||
|
<td>Database Size</td>
|
||||||
|
<td id="db-size">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Total Events</td>
|
||||||
|
<td id="total-events">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Oldest Event</td>
|
||||||
|
<td id="oldest-event">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Newest Event</td>
|
||||||
|
<td id="newest-event">-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Kind Distribution Table -->
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Event Kind Distribution:</label>
|
||||||
|
<div class="config-table-container">
|
||||||
|
<table class="config-table" id="stats-kinds-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Event Kind</th>
|
||||||
|
<th>Count</th>
|
||||||
|
<th>Percentage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="stats-kinds-table-body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="text-align: center; font-style: italic;">No data loaded</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time-based Statistics Table -->
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Time-based Statistics:</label>
|
||||||
|
<div class="config-table-container">
|
||||||
|
<table class="config-table" id="stats-time-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Period</th>
|
||||||
|
<th>Events</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="stats-time-table-body">
|
||||||
|
<tr>
|
||||||
|
<td>Last 24 Hours</td>
|
||||||
|
<td id="events-24h">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Last 7 Days</td>
|
||||||
|
<td id="events-7d">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Last 30 Days</td>
|
||||||
|
<td id="events-30d">-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Pubkeys Table -->
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Top Pubkeys by Event Count:</label>
|
||||||
|
<div class="config-table-container">
|
||||||
|
<table class="config-table" id="stats-pubkeys-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Pubkey</th>
|
||||||
|
<th>Event Count</th>
|
||||||
|
<th>Percentage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="stats-pubkeys-table-body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" style="text-align: center; font-style: italic;">No data loaded</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Testing Section -->
|
||||||
|
<div id="div_config" class="section flex-section" style="display: none;">
|
||||||
|
<h2>RELAY CONFIGURATION</h2>
|
||||||
|
<div id="config-display" class="hidden">
|
||||||
|
<div class="config-table-container">
|
||||||
|
<table class="config-table" id="config-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="config-table-body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-buttons">
|
||||||
|
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Rules Management - Moved after configuration -->
|
||||||
|
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>AUTH RULES MANAGEMENT</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Rules Table -->
|
||||||
|
<div id="authRulesTableContainer" style="display: none;">
|
||||||
|
<table class="config-table" id="authRulesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rule Type</th>
|
||||||
|
<th>Pattern Type</th>
|
||||||
|
<th>Pattern Value</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="authRulesTableBody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simplified Auth Rule Input Section -->
|
||||||
|
<div id="authRuleInputSections" style="display: block;">
|
||||||
|
|
||||||
|
<!-- Combined Pubkey Auth Rule Section -->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
||||||
|
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
||||||
|
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
|
||||||
|
mode.
|
||||||
|
Only whitelisted users will be able to interact with the relay.
|
||||||
|
</div>
|
||||||
|
<div class="inline-buttons">
|
||||||
|
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
|
||||||
|
WHITELIST</button>
|
||||||
|
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
|
||||||
|
BLACKLIST</button>
|
||||||
|
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- NIP-17 DIRECT MESSAGES Section -->
|
||||||
|
<div class="section" id="nip17DMSection" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>NIP-17 DIRECT MESSAGES</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Outbox -->
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="dm-outbox">Send Message to Relay:</label>
|
||||||
|
<textarea id="dm-outbox" rows="4" placeholder="Enter your message to send to the relay..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Send Button -->
|
||||||
|
<div class="input-group">
|
||||||
|
<button type="button" id="send-dm-btn">SEND MESSAGE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inbox -->
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Received Messages from Relay:</label>
|
||||||
|
<div id="dm-inbox" class="log-panel" style="height: 200px;">
|
||||||
|
<div class="log-entry">No messages received yet.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Load the official nostr-tools bundle first -->
|
||||||
|
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> -->
|
||||||
|
<script src="/api/nostr.bundle.js"></script>
|
||||||
|
|
||||||
|
<!-- Load NOSTR_LOGIN_LITE main library -->
|
||||||
|
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> -->
|
||||||
|
<script src="/api/nostr-lite.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/api/index.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3921
api/index.js
Normal file
3921
api/index.js
Normal file
File diff suppressed because it is too large
Load Diff
4282
api/nostr-lite.js
Normal file
4282
api/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
11534
api/nostr.bundle.js
Normal file
11534
api/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
207
build_static.sh
Executable file
207
build_static.sh
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build fully static MUSL binaries for C-Relay using Alpine Docker
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/build"
|
||||||
|
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
DEBUG_BUILD=false
|
||||||
|
if [[ "$1" == "--debug" ]]; then
|
||||||
|
DEBUG_BUILD=true
|
||||||
|
echo "=========================================="
|
||||||
|
echo "C-Relay MUSL Static Binary Builder (DEBUG MODE)"
|
||||||
|
echo "=========================================="
|
||||||
|
else
|
||||||
|
echo "=========================================="
|
||||||
|
echo "C-Relay MUSL Static Binary Builder (PRODUCTION MODE)"
|
||||||
|
echo "=========================================="
|
||||||
|
fi
|
||||||
|
echo "Project directory: $SCRIPT_DIR"
|
||||||
|
echo "Build directory: $BUILD_DIR"
|
||||||
|
echo "Debug build: $DEBUG_BUILD"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Check if Docker is available
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "ERROR: Docker is not installed or not in PATH"
|
||||||
|
echo ""
|
||||||
|
echo "Docker is required to build MUSL static binaries."
|
||||||
|
echo "Please install Docker:"
|
||||||
|
echo " - Ubuntu/Debian: sudo apt install docker.io"
|
||||||
|
echo " - Or visit: https://docs.docker.com/engine/install/"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker daemon is running
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
echo "ERROR: Docker daemon is not running or user not in docker group"
|
||||||
|
echo ""
|
||||||
|
echo "Please start Docker and ensure you're in the docker group:"
|
||||||
|
echo " - sudo systemctl start docker"
|
||||||
|
echo " - sudo usermod -aG docker $USER && newgrp docker"
|
||||||
|
echo " - Or start Docker Desktop"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOCKER_CMD="docker"
|
||||||
|
|
||||||
|
echo "✓ Docker is available and running"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="c_relay_static_x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
OUTPUT_NAME="c_relay_static_arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WARNING: Unknown architecture: $ARCH"
|
||||||
|
echo "Defaulting to linux/amd64"
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="c_relay_static_${ARCH}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Building for platform: $PLATFORM"
|
||||||
|
echo "Output binary: $OUTPUT_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 1: Building Alpine Docker image"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "This will:"
|
||||||
|
echo " - Use Alpine Linux (native MUSL)"
|
||||||
|
echo " - Build all dependencies statically"
|
||||||
|
echo " - Compile c-relay with full static linking"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
$DOCKER_CMD build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t c-relay-musl-builder:latest \
|
||||||
|
--progress=plain \
|
||||||
|
. || {
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: Docker build failed"
|
||||||
|
echo "Check the output above for details"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Docker image built successfully"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract the binary from the container
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 2: Extracting static binary"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Build the builder stage to extract the binary
|
||||||
|
$DOCKER_CMD build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||||
|
--target builder \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t c-relay-static-builder-stage:latest \
|
||||||
|
. > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Create a temporary container to copy the binary
|
||||||
|
CONTAINER_ID=$($DOCKER_CMD create c-relay-static-builder-stage:latest)
|
||||||
|
|
||||||
|
# Copy binary from container
|
||||||
|
$DOCKER_CMD cp "$CONTAINER_ID:/build/c_relay_static" "$BUILD_DIR/$OUTPUT_NAME" || {
|
||||||
|
echo "ERROR: Failed to extract binary from container"
|
||||||
|
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up container
|
||||||
|
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
|
||||||
|
|
||||||
|
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Make binary executable
|
||||||
|
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
# Verify the binary
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Step 3: Verifying static binary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Checking for dynamic dependencies:"
|
||||||
|
if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
|
||||||
|
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
|
||||||
|
echo "✓ Binary is fully static (no dynamic dependencies)"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
|
||||||
|
echo "✓ Binary is statically linked"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
else
|
||||||
|
echo "⚠ WARNING: Binary may have dynamic dependencies:"
|
||||||
|
echo "$LDD_OUTPUT"
|
||||||
|
TRULY_STATIC=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# ldd failed or timed out - check with file command instead
|
||||||
|
if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
|
||||||
|
echo "✓ Binary is statically linked (verified with file command)"
|
||||||
|
TRULY_STATIC=true
|
||||||
|
else
|
||||||
|
echo "⚠ Could not verify static linking (ldd check failed)"
|
||||||
|
TRULY_STATIC=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test if binary runs
|
||||||
|
echo "Testing binary execution:"
|
||||||
|
if "$BUILD_DIR/$OUTPUT_NAME" --version 2>&1 | head -5; then
|
||||||
|
echo "✓ Binary executes successfully"
|
||||||
|
else
|
||||||
|
echo "⚠ Binary execution test failed (this may be normal if --version is not supported)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Build Summary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||||
|
echo "Platform: $PLATFORM"
|
||||||
|
if [ "$DEBUG_BUILD" = true ]; then
|
||||||
|
echo "Build Type: DEBUG (with symbols, no optimization)"
|
||||||
|
else
|
||||||
|
echo "Build Type: PRODUCTION (optimized, stripped)"
|
||||||
|
fi
|
||||||
|
if [ "$TRULY_STATIC" = true ]; then
|
||||||
|
echo "Linkage: Fully static binary (Alpine MUSL-based)"
|
||||||
|
echo "Portability: Works on ANY Linux distribution"
|
||||||
|
else
|
||||||
|
echo "Linkage: Static binary (may have minimal dependencies)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "✓ Build complete!"
|
||||||
|
echo ""
|
||||||
8
c-relay.code-workspace
Normal file
8
c-relay.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
1
c_utils_lib
Submodule
1
c_utils_lib
Submodule
Submodule c_utils_lib added at 442facd7e3
@@ -1 +0,0 @@
|
|||||||
Only README.md will remain
|
|
||||||
3
deploy_local.sh
Executable file
3
deploy_local.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
||||||
28
deploy_static.sh
Executable file
28
deploy_static.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# C-Relay Static Binary Deployment Script
|
||||||
|
# Deploys build/c_relay_static_x86_64 to server via ssh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
LOCAL_BINARY="build/c_relay_static_x86_64"
|
||||||
|
REMOTE_BINARY_PATH="/usr/local/bin/c_relay/c_relay"
|
||||||
|
SERVICE_NAME="c-relay"
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
ssh ubuntu@laantungir.com "sudo cp '$REMOTE_BINARY_PATH' '${REMOTE_BINARY_PATH}.backup.$(date +%Y%m%d_%H%M%S)'" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Upload binary to temp location
|
||||||
|
scp "$LOCAL_BINARY" "ubuntu@laantungir.com:/tmp/c_relay.tmp"
|
||||||
|
|
||||||
|
# Install binary
|
||||||
|
ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
|
||||||
|
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
|
||||||
|
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
|
||||||
|
|
||||||
|
# Reload systemd and restart service
|
||||||
|
ssh ubuntu@laantungir.com "sudo systemctl daemon-reload"
|
||||||
|
ssh ubuntu@laantungir.com "sudo systemctl restart '$SERVICE_NAME'"
|
||||||
|
|
||||||
|
echo "Deployment complete!"
|
||||||
295
docs/NIP-42_Authentication.md
Normal file
295
docs/NIP-42_Authentication.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# NIP-42 Authentication Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This relay implements NIP-42 (Authentication of clients to relays) providing granular authentication controls for event submission and subscription operations. The implementation supports both challenge-response authentication and per-connection state management.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Per-Session Authentication State** (`struct per_session_data`)
|
||||||
|
- `authenticated`: Boolean flag indicating authentication status
|
||||||
|
- `authenticated_pubkey[65]`: Hex-encoded public key of authenticated user
|
||||||
|
- `active_challenge[65]`: Current authentication challenge
|
||||||
|
- `challenge_created`: Timestamp when challenge was generated
|
||||||
|
- `challenge_expires`: Challenge expiration timestamp
|
||||||
|
- `nip42_auth_required_events`: Whether auth is required for EVENT submission
|
||||||
|
- `nip42_auth_required_subscriptions`: Whether auth is required for REQ operations
|
||||||
|
- `auth_challenge_sent`: Flag indicating if challenge has been sent
|
||||||
|
|
||||||
|
2. **Challenge Management** (via `request_validator.c`)
|
||||||
|
- `nostr_nip42_generate_challenge()`: Generates cryptographically secure challenges
|
||||||
|
- `nostr_nip42_verify_auth_event()`: Validates signed authentication events
|
||||||
|
- Challenge storage and cleanup with expiration handling
|
||||||
|
|
||||||
|
3. **WebSocket Protocol Integration**
|
||||||
|
- AUTH message handling in `nostr_relay_callback()`
|
||||||
|
- Challenge generation and transmission
|
||||||
|
- Authentication verification and session state updates
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Event-Based Configuration
|
||||||
|
|
||||||
|
NIP-42 authentication is configured using kind 33334 configuration events with the following tags:
|
||||||
|
|
||||||
|
| Tag | Description | Default | Values |
|
||||||
|
|-----|-------------|---------|--------|
|
||||||
|
| `nip42_auth_required_events` | Require auth for EVENT submission | `false` | `true`/`false` |
|
||||||
|
| `nip42_auth_required_subscriptions` | Require auth for REQ operations | `false` | `true`/`false` |
|
||||||
|
|
||||||
|
### Example Configuration Event
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 33334,
|
||||||
|
"content": "C Nostr Relay Configuration",
|
||||||
|
"tags": [
|
||||||
|
["d", "<relay_pubkey>"],
|
||||||
|
["nip42_auth_required_events", "true"],
|
||||||
|
["nip42_auth_required_subscriptions", "false"],
|
||||||
|
["relay_description", "Authenticated Nostr Relay"]
|
||||||
|
],
|
||||||
|
"created_at": 1640995200,
|
||||||
|
"pubkey": "<admin_pubkey>",
|
||||||
|
"id": "<event_id>",
|
||||||
|
"sig": "<signature>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
### 1. Challenge Generation
|
||||||
|
|
||||||
|
When authentication is required and client is not authenticated:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client -> Relay: ["EVENT", <event>] (unauthenticated)
|
||||||
|
Relay -> Client: ["AUTH", <challenge>]
|
||||||
|
```
|
||||||
|
|
||||||
|
The challenge is a 64-character hex string generated using cryptographically secure random numbers.
|
||||||
|
|
||||||
|
### 2. Authentication Response
|
||||||
|
|
||||||
|
Client creates and signs an authentication event (kind 22242):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 22242,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["relay", "ws://relay.example.com"],
|
||||||
|
["challenge", "<challenge_from_relay>"]
|
||||||
|
],
|
||||||
|
"created_at": <current_timestamp>,
|
||||||
|
"pubkey": "<client_pubkey>",
|
||||||
|
"id": "<event_id>",
|
||||||
|
"sig": "<signature>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Client sends this event back to relay:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client -> Relay: ["AUTH", <signed_auth_event>]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verification and Session Update
|
||||||
|
|
||||||
|
The relay:
|
||||||
|
1. Validates the authentication event signature
|
||||||
|
2. Verifies the challenge matches the one sent
|
||||||
|
3. Checks challenge expiration (default: 10 minutes)
|
||||||
|
4. Updates session state with authenticated public key
|
||||||
|
5. Sends confirmation notice
|
||||||
|
|
||||||
|
```
|
||||||
|
Relay -> Client: ["NOTICE", "NIP-42 authentication successful"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Granular Authentication Controls
|
||||||
|
|
||||||
|
### Separate Controls for Events vs Subscriptions
|
||||||
|
|
||||||
|
The implementation provides separate authentication requirements:
|
||||||
|
|
||||||
|
- **Event Submission**: Control whether clients must authenticate to publish events
|
||||||
|
- **Subscription Access**: Control whether clients must authenticate to create subscriptions
|
||||||
|
|
||||||
|
This allows flexible relay policies:
|
||||||
|
- **Public Read, Authenticated Write**: `events=true, subscriptions=false`
|
||||||
|
- **Fully Authenticated**: `events=true, subscriptions=true`
|
||||||
|
- **Public Access**: `events=false, subscriptions=false` (default)
|
||||||
|
- **Authenticated Read Only**: `events=false, subscriptions=true`
|
||||||
|
|
||||||
|
### Per-Connection State
|
||||||
|
|
||||||
|
Each WebSocket connection maintains its own authentication state:
|
||||||
|
- Authentication persists for the lifetime of the connection
|
||||||
|
- Challenges expire after 10 minutes
|
||||||
|
- Session cleanup on connection close
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Challenge Security
|
||||||
|
- 64-character hexadecimal challenges (256 bits of entropy)
|
||||||
|
- Cryptographically secure random generation
|
||||||
|
- Challenge expiration to prevent replay attacks
|
||||||
|
- One-time use challenges
|
||||||
|
|
||||||
|
### Event Validation
|
||||||
|
- Complete signature verification using secp256k1
|
||||||
|
- Event ID validation
|
||||||
|
- Challenge-response binding verification
|
||||||
|
- Timestamp validation with configurable tolerance
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
- Thread-safe per-session state management
|
||||||
|
- Automatic cleanup on disconnection
|
||||||
|
- Challenge expiration handling
|
||||||
|
|
||||||
|
## Client Integration
|
||||||
|
|
||||||
|
### Using nak Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate keypair
|
||||||
|
PRIVKEY=$(nak key --gen)
|
||||||
|
PUBKEY=$(nak key --pub $PRIVKEY)
|
||||||
|
|
||||||
|
# Connect and authenticate automatically
|
||||||
|
nak event -k 1 --content "Authenticated message" --sec $PRIVKEY --relay ws://localhost:8888
|
||||||
|
|
||||||
|
# nak handles NIP-42 authentication automatically when required
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual WebSocket Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8888');
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (message[0] === 'AUTH') {
|
||||||
|
const challenge = message[1];
|
||||||
|
|
||||||
|
// Create auth event (kind 22242)
|
||||||
|
const authEvent = {
|
||||||
|
kind: 22242,
|
||||||
|
content: "",
|
||||||
|
tags: [
|
||||||
|
["relay", "ws://localhost:8888"],
|
||||||
|
["challenge", challenge]
|
||||||
|
],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
pubkey: clientPubkey,
|
||||||
|
// ... calculate id and signature
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send auth response
|
||||||
|
ws.send(JSON.stringify(["AUTH", authEvent]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send event (may trigger AUTH challenge)
|
||||||
|
ws.send(JSON.stringify(["EVENT", myEvent]));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Administration
|
||||||
|
|
||||||
|
### Enabling Authentication
|
||||||
|
|
||||||
|
1. **Get Admin Private Key**: Extract from relay startup logs (shown once)
|
||||||
|
2. **Create Configuration Event**: Use nak or custom tooling
|
||||||
|
3. **Publish Configuration**: Send to relay with admin signature
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable auth for events only
|
||||||
|
nak event -k 33334 \
|
||||||
|
--content "C Nostr Relay Configuration" \
|
||||||
|
--tag "d=$RELAY_PUBKEY" \
|
||||||
|
--tag "nip42_auth_required_events=true" \
|
||||||
|
--tag "nip42_auth_required_subscriptions=false" \
|
||||||
|
--sec $ADMIN_PRIVKEY \
|
||||||
|
--relay ws://localhost:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring Authentication
|
||||||
|
|
||||||
|
- Check relay logs for authentication events
|
||||||
|
- Monitor `NOTICE` messages for auth status
|
||||||
|
- Use `get_settings.sh` script to view current configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./get_settings.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Challenge Expiration**
|
||||||
|
- Default: 10 minutes
|
||||||
|
- Client must respond within expiration window
|
||||||
|
- Generate new challenge for expired attempts
|
||||||
|
|
||||||
|
2. **Signature Verification Failures**
|
||||||
|
- Verify event structure matches NIP-42 specification
|
||||||
|
- Check challenge value matches exactly
|
||||||
|
- Ensure proper secp256k1 signature generation
|
||||||
|
|
||||||
|
3. **Configuration Not Applied**
|
||||||
|
- Verify admin private key is correct
|
||||||
|
- Check configuration event signature
|
||||||
|
- Ensure relay pubkey in 'd' tag matches relay
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check supported NIPs
|
||||||
|
curl -H "Accept: application/nostr+json" http://localhost:8888 | jq .supported_nips
|
||||||
|
|
||||||
|
# View current configuration
|
||||||
|
nak req -k 33334 ws://localhost:8888 | jq .
|
||||||
|
|
||||||
|
# Test authentication flow
|
||||||
|
./tests/42_nip_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Challenge generation: ~1ms overhead per unauthenticated connection
|
||||||
|
- Authentication verification: ~2-5ms per auth event
|
||||||
|
- Memory overhead: ~200 bytes per connection for auth state
|
||||||
|
- Database impact: Configuration events cached, minimal query overhead
|
||||||
|
|
||||||
|
## Integration with Other NIPs
|
||||||
|
|
||||||
|
### NIP-01 (Basic Protocol)
|
||||||
|
- AUTH messages integrated into standard WebSocket flow
|
||||||
|
- Compatible with existing EVENT/REQ/CLOSE message handling
|
||||||
|
|
||||||
|
### NIP-11 (Relay Information)
|
||||||
|
- NIP-42 advertised in `supported_nips` array
|
||||||
|
- Authentication requirements reflected in relay metadata
|
||||||
|
|
||||||
|
### NIP-20 (Command Results)
|
||||||
|
- OK responses include authentication-related error messages
|
||||||
|
- NOTICE messages provide authentication status updates
|
||||||
|
|
||||||
|
## Future Extensions
|
||||||
|
|
||||||
|
### Potential Enhancements
|
||||||
|
- Role-based authentication (admin, user, read-only)
|
||||||
|
- Time-based access controls
|
||||||
|
- Rate limiting based on authentication status
|
||||||
|
- Integration with external authentication providers
|
||||||
|
|
||||||
|
### Configuration Extensions
|
||||||
|
- Per-kind authentication requirements
|
||||||
|
- Whitelist/blacklist integration
|
||||||
|
- Custom challenge expiration times
|
||||||
|
- Authentication logging and metrics
|
||||||
460
docs/admin_api_plan.md
Normal file
460
docs/admin_api_plan.md
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
# C-Relay Administrator API Implementation Plan
|
||||||
|
|
||||||
|
## Problem Analysis
|
||||||
|
|
||||||
|
### Current Issues Identified:
|
||||||
|
|
||||||
|
1. **Schema Mismatch**: Storage system (config.c) vs Validation system (request_validator.c) use different column names and values
|
||||||
|
2. **Missing API Endpoint**: No way to clear auth_rules table for testing
|
||||||
|
3. **Configuration Gap**: Auth rules enforcement may not be properly enabled
|
||||||
|
4. **Documentation Gap**: Admin API commands not documented
|
||||||
|
|
||||||
|
### Root Cause: Auth Rules Schema Inconsistency
|
||||||
|
|
||||||
|
**Current Schema (sql_schema.h lines 140-150):**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE auth_rules (
|
||||||
|
rule_type TEXT CHECK (rule_type IN ('whitelist', 'blacklist')),
|
||||||
|
pattern_type TEXT CHECK (pattern_type IN ('pubkey', 'hash')),
|
||||||
|
pattern_value TEXT,
|
||||||
|
action TEXT CHECK (action IN ('allow', 'deny')),
|
||||||
|
active INTEGER DEFAULT 1
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Storage Implementation (config.c):**
|
||||||
|
- Stores: `rule_type='blacklist'`, `pattern_type='pubkey'`, `pattern_value='hex'`, `action='allow'`
|
||||||
|
|
||||||
|
**Validation Implementation (request_validator.c):**
|
||||||
|
- Queries: `rule_type='pubkey_blacklist'`, `rule_target='hex'`, `operation='event'`, `enabled=1`
|
||||||
|
|
||||||
|
**MISMATCH**: Validator looks for non-existent columns and wrong rule_type values!
|
||||||
|
|
||||||
|
## Proposed Solution Architecture
|
||||||
|
|
||||||
|
### Phase 1: API Documentation & Standardization
|
||||||
|
|
||||||
|
#### Admin API Commands (via WebSocket with admin private key)
|
||||||
|
|
||||||
|
**Kind 23456: Unified Admin API (Ephemeral)**
|
||||||
|
- Configuration management: Update relay settings, limits, authentication policies
|
||||||
|
- Auth rules: Add/remove/query whitelist/blacklist rules
|
||||||
|
- System commands: clear rules, status, cache management
|
||||||
|
- **Unified Format**: All commands use NIP-44 encrypted content with `["p", "relay_pubkey"]` tags
|
||||||
|
- **Command Types**:
|
||||||
|
- Configuration: `["config_key", "config_value"]`
|
||||||
|
- Auth rules: `["rule_type", "pattern_type", "pattern_value"]`
|
||||||
|
- Queries: `["auth_query", "filter"]` or `["system_command", "command_name"]`
|
||||||
|
- **Security**: All admin commands use NIP-44 encryption for privacy and security
|
||||||
|
|
||||||
|
#### Configuration Commands (using Kind 23456)
|
||||||
|
|
||||||
|
1. **Update Configuration**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["relay_description", "My Relay"]`
|
||||||
|
|
||||||
|
2. **Query System Status**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["system_command", "system_status"]`
|
||||||
|
|
||||||
|
#### Auth Rules and System Commands (using Kind 23456)
|
||||||
|
|
||||||
|
1. **Clear All Auth Rules**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["system_command", "clear_all_auth_rules"]`
|
||||||
|
|
||||||
|
2. **Query All Auth Rules**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["auth_query", "all"]`
|
||||||
|
|
||||||
|
3. **Add Blacklist Rule**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["blacklist", "pubkey", "deadbeef1234abcd..."]`
|
||||||
|
|
||||||
|
### Phase 2: Auth Rules Schema Alignment
|
||||||
|
|
||||||
|
#### Option A: Fix Validator to Match Schema (RECOMMENDED)
|
||||||
|
|
||||||
|
**Update request_validator.c:**
|
||||||
|
```sql
|
||||||
|
-- OLD (broken):
|
||||||
|
WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = 1
|
||||||
|
|
||||||
|
-- NEW (correct):
|
||||||
|
WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Matches actual database schema
|
||||||
|
- Simpler rule_type values ('blacklist' vs 'pubkey_blacklist')
|
||||||
|
- Uses existing columns (pattern_value vs rule_target)
|
||||||
|
- Consistent with storage implementation
|
||||||
|
|
||||||
|
#### Option B: Update Schema to Match Validator (NOT RECOMMENDED)
|
||||||
|
|
||||||
|
Would require changing schema, migration scripts, and storage logic.
|
||||||
|
|
||||||
|
### Phase 3: Implementation Priority
|
||||||
|
|
||||||
|
#### High Priority (Critical for blacklist functionality):
|
||||||
|
1. Fix request_validator.c schema mismatch
|
||||||
|
2. Ensure auth_required configuration is enabled
|
||||||
|
3. Update tests to use unified ephemeral event kind (23456)
|
||||||
|
4. Test blacklist enforcement
|
||||||
|
|
||||||
|
#### Medium Priority (Enhanced Admin Features):
|
||||||
|
1. **Implement NIP-44 Encryption Support**:
|
||||||
|
- Detect NIP-44 encrypted content for Kind 23456 events
|
||||||
|
- Parse `encrypted_tags` field from content JSON
|
||||||
|
- Decrypt using admin privkey and relay pubkey
|
||||||
|
- Process decrypted tags as normal commands
|
||||||
|
2. Add clear_all_auth_rules system command
|
||||||
|
3. Add auth rule query functionality (both standard and encrypted modes)
|
||||||
|
4. Add configuration discovery (list available config keys)
|
||||||
|
5. Enhanced error reporting in admin API
|
||||||
|
6. Conflict resolution (same pubkey in whitelist + blacklist)
|
||||||
|
|
||||||
|
#### Security Priority (NIP-44 Implementation):
|
||||||
|
1. **Encryption Detection Logic**: Check for empty tags + encrypted_tags field
|
||||||
|
2. **Key Pair Management**: Use admin private key + relay public key for NIP-44
|
||||||
|
3. **Backward Compatibility**: Support both standard and encrypted modes
|
||||||
|
4. **Error Handling**: Graceful fallback if decryption fails
|
||||||
|
5. **Performance**: Cache decrypted results to avoid repeated decryption
|
||||||
|
|
||||||
|
#### Low Priority (Documentation & Polish):
|
||||||
|
1. Complete README.md API documentation
|
||||||
|
2. Example usage scripts
|
||||||
|
3. Admin client tools
|
||||||
|
|
||||||
|
### Phase 4: Expected API Structure
|
||||||
|
|
||||||
|
#### README.md Documentation Format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# C-Relay Administrator API
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
All admin commands require signing with the admin private key generated during first startup.
|
||||||
|
|
||||||
|
## Unified Admin API (Kind 23456 - Ephemeral)
|
||||||
|
Update relay configuration parameters or query available settings.
|
||||||
|
|
||||||
|
**Configuration Update Event:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "base64_nip44_encrypted_command_array",
|
||||||
|
"tags": [["p", "relay_pubkey"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Encrypted content contains:* `["relay_description", "My Relay Description"]`
|
||||||
|
|
||||||
|
**Auth Rules Management:**
|
||||||
|
|
||||||
|
**Add Rule Event:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"action\":\"add\",\"description\":\"Block malicious user\"}",
|
||||||
|
"tags": [
|
||||||
|
["blacklist", "pubkey", "deadbeef1234..."]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remove Rule Event:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"action\":\"remove\",\"description\":\"Unblock user\"}",
|
||||||
|
"tags": [
|
||||||
|
["blacklist", "pubkey", "deadbeef1234..."]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query All Auth Rules:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get all rules\"}",
|
||||||
|
"tags": [
|
||||||
|
["auth_query", "all"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Whitelist Rules Only:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get whitelist\"}",
|
||||||
|
"tags": [
|
||||||
|
["auth_query", "whitelist"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check Specific Pattern:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"query\":\"check_pattern\",\"description\":\"Check if pattern exists\"}",
|
||||||
|
"tags": [
|
||||||
|
["auth_query", "pattern", "deadbeef1234..."]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Management (Kind 23456 - Ephemeral)
|
||||||
|
System administration commands using the same kind as auth rules.
|
||||||
|
|
||||||
|
**Clear All Auth Rules:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"action\":\"clear_all\",\"description\":\"Clear all auth rules\"}",
|
||||||
|
"tags": [
|
||||||
|
["system_command", "clear_all_auth_rules"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**System Status:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"action\":\"system_status\",\"description\":\"Get system status\"}",
|
||||||
|
"tags": [
|
||||||
|
["system_command", "system_status"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
All admin commands return JSON responses via WebSocket:
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
["OK", "event_id", true, "success_message"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response:**
|
||||||
|
```json
|
||||||
|
["OK", "event_id", false, "error_message"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Keys
|
||||||
|
- `relay_description`: Relay description text
|
||||||
|
- `relay_contact`: Contact information
|
||||||
|
- `auth_enabled`: Enable authentication system
|
||||||
|
- `max_connections`: Maximum concurrent connections
|
||||||
|
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||||
|
- ... (full list of config keys)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Enable Authentication & Add Blacklist
|
||||||
|
```bash
|
||||||
|
# 1. Enable auth system
|
||||||
|
nak event -k 23456 --content "base64_nip44_encrypted_command" \
|
||||||
|
-t "auth_enabled=true" \
|
||||||
|
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||||
|
|
||||||
|
# 2. Add user to blacklist
|
||||||
|
nak event -k 23456 --content '{"action":"add","description":"Spam user"}' \
|
||||||
|
-t "blacklist=pubkey;$SPAM_USER_PUBKEY" \
|
||||||
|
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||||
|
|
||||||
|
# 3. Query all auth rules
|
||||||
|
nak event -k 23456 --content '{"query":"list_auth_rules","description":"Get all rules"}' \
|
||||||
|
-t "auth_query=all" \
|
||||||
|
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||||
|
|
||||||
|
# 4. Clear all rules for testing
|
||||||
|
nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rules"}' \
|
||||||
|
-t "system_command=clear_all_auth_rules" \
|
||||||
|
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Response Formats
|
||||||
|
|
||||||
|
### Configuration Query Response
|
||||||
|
```json
|
||||||
|
["EVENT", "subscription_id", {
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "base64_nip44_encrypted_response",
|
||||||
|
"tags": [["p", "admin_pubkey"]]
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Config Response
|
||||||
|
```json
|
||||||
|
["EVENT", "subscription_id", {
|
||||||
|
"kind": 23457,
|
||||||
|
"content": "base64_nip44_encrypted_response",
|
||||||
|
"tags": [["p", "admin_pubkey"]]
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Rules Query Response
|
||||||
|
```json
|
||||||
|
["EVENT", "subscription_id", {
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"auth_rules\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"deadbeef...\"}, {\"rule_type\": \"whitelist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"cafebabe...\"}]}",
|
||||||
|
"tags": [["response_type", "auth_rules_list"], ["query_type", "all"]]
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern Check Response
|
||||||
|
```json
|
||||||
|
["EVENT", "subscription_id", {
|
||||||
|
"kind": 23456,
|
||||||
|
"content": "{\"pattern_exists\": true, \"rule_type\": \"blacklist\", \"pattern_value\": \"deadbeef...\"}",
|
||||||
|
"tags": [["response_type", "pattern_check"], ["pattern", "deadbeef..."]]
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **Document API** (this file) ✅
|
||||||
|
2. **Update to ephemeral event kinds** ✅
|
||||||
|
3. **Fix request_validator.c** schema mismatch
|
||||||
|
4. **Update tests** to use unified Kind 23456
|
||||||
|
5. **Add auth rule query functionality**
|
||||||
|
6. **Add configuration discovery feature**
|
||||||
|
7. **Test blacklist functionality**
|
||||||
|
8. **Add remaining system commands**
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
1. Fix schema mismatch and test basic blacklist
|
||||||
|
2. Add clear_auth_rules and test table cleanup
|
||||||
|
3. Test whitelist/blacklist conflict scenarios
|
||||||
|
4. Test all admin API commands end-to-end
|
||||||
|
5. Update integration tests
|
||||||
|
|
||||||
|
This plan addresses the immediate blacklist issue while establishing a comprehensive admin API framework for future expansion.
|
||||||
|
|
||||||
|
## NIP-44 Encryption Implementation Details
|
||||||
|
|
||||||
|
### Server-Side Detection Logic
|
||||||
|
```c
|
||||||
|
// In admin event processing function
|
||||||
|
bool is_encrypted_command(struct nostr_event *event) {
|
||||||
|
// Check if Kind 23456 with NIP-44 encrypted content
|
||||||
|
if (event->kind == 23456 &&
|
||||||
|
event->tags_count == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *decrypt_admin_tags(struct nostr_event *event) {
|
||||||
|
cJSON *content_json = cJSON_Parse(event->content);
|
||||||
|
if (!content_json) return NULL;
|
||||||
|
|
||||||
|
cJSON *encrypted_tags = cJSON_GetObjectItem(content_json, "encrypted_tags");
|
||||||
|
if (!encrypted_tags) {
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt using NIP-44 with admin pubkey and relay privkey
|
||||||
|
char *decrypted = nip44_decrypt(
|
||||||
|
cJSON_GetStringValue(encrypted_tags),
|
||||||
|
admin_pubkey, // Shared secret with admin
|
||||||
|
relay_private_key // Our private key
|
||||||
|
);
|
||||||
|
|
||||||
|
cJSON *decrypted_tags = cJSON_Parse(decrypted);
|
||||||
|
free(decrypted);
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
|
||||||
|
return decrypted_tags; // Returns tag array: [["key1", "val1"], ["key2", "val2"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Event Processing Flow
|
||||||
|
1. **Receive Event**: Kind 23456 with admin signature
|
||||||
|
2. **Check Mode**: Empty tags = encrypted, populated tags = standard
|
||||||
|
3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content
|
||||||
|
4. **Process Commands**: Use decrypted/standard tags for command processing
|
||||||
|
5. **Execute**: Same logic for both modes after tag extraction
|
||||||
|
6. **Respond**: Standard response format (optionally encrypt response)
|
||||||
|
|
||||||
|
### Security Benefits
|
||||||
|
- **Command Privacy**: Admin operations invisible in event tags
|
||||||
|
- **Replay Protection**: NIP-44 includes timestamp/randomness
|
||||||
|
- **Key Management**: Uses existing admin/relay key pair
|
||||||
|
- **Backward Compatible**: Standard mode still works
|
||||||
|
- **Performance**: Only decrypt when needed (empty tags detection)
|
||||||
|
|
||||||
|
### NIP-44 Library Integration
|
||||||
|
The relay will need to integrate a NIP-44 encryption/decryption library:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Required NIP-44 functions
|
||||||
|
char* nip44_encrypt(const char* plaintext, const char* sender_privkey, const char* recipient_pubkey);
|
||||||
|
char* nip44_decrypt(const char* ciphertext, const char* recipient_privkey, const char* sender_pubkey);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Priority (Updated)
|
||||||
|
|
||||||
|
#### Phase 1: Core Infrastructure (Complete)
|
||||||
|
- [x] Event-based admin authentication system
|
||||||
|
- [x] Kind 23456 (Unified Admin API) processing
|
||||||
|
- [x] Basic configuration parameter updates
|
||||||
|
- [x] Auth rule add/remove/clear functionality
|
||||||
|
- [x] Updated to ephemeral event kinds
|
||||||
|
- [x] Designed NIP-44 encryption support
|
||||||
|
|
||||||
|
#### Phase 2: NIP-44 Encryption Support (Next Priority)
|
||||||
|
- [ ] **Add NIP-44 library dependency** to project
|
||||||
|
- [ ] **Implement encryption detection logic** (`is_encrypted_command()`)
|
||||||
|
- [ ] **Add decrypt_admin_tags() function** with NIP-44 support
|
||||||
|
- [ ] **Update admin command processing** to handle both modes
|
||||||
|
- [ ] **Test encrypted admin commands** end-to-end
|
||||||
|
|
||||||
|
#### Phase 3: Enhanced Features
|
||||||
|
- [ ] **Auth rule query functionality** (both standard and encrypted modes)
|
||||||
|
- [ ] **Configuration discovery API** (list available config keys)
|
||||||
|
- [ ] **Enhanced error messages** with encryption status
|
||||||
|
- [ ] **Performance optimization** (caching, async decrypt)
|
||||||
|
|
||||||
|
#### Phase 4: Schema Fixes (Critical)
|
||||||
|
- [ ] **Fix request_validator.c** schema mismatch
|
||||||
|
- [ ] **Enable blacklist enforcement** with encrypted commands
|
||||||
|
- [ ] **Update tests** to use both standard and encrypted modes
|
||||||
|
|
||||||
|
This enhanced admin API provides enterprise-grade security while maintaining ease of use for basic operations.
|
||||||
457
docs/c_utils_lib_architecture.md
Normal file
457
docs/c_utils_lib_architecture.md
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
# c_utils_lib Architecture Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`c_utils_lib` is a standalone C utility library designed to provide reusable, general-purpose functions for C projects. It serves as a learning repository and a practical toolkit for common C programming tasks.
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
1. **Zero External Dependencies**: Only standard C library dependencies
|
||||||
|
2. **Modular Design**: Each utility is independent and can be used separately
|
||||||
|
3. **Learning-Oriented**: Well-documented code suitable for learning C
|
||||||
|
4. **Production-Ready**: Battle-tested utilities from real projects
|
||||||
|
5. **Cross-Platform**: Works on Linux, macOS, and other POSIX systems
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
c_utils_lib/
|
||||||
|
├── README.md # Main documentation
|
||||||
|
├── LICENSE # MIT License
|
||||||
|
├── VERSION # Current version (e.g., v0.1.0)
|
||||||
|
├── build.sh # Build script
|
||||||
|
├── Makefile # Build system
|
||||||
|
├── .gitignore # Git ignore rules
|
||||||
|
│
|
||||||
|
├── include/ # Public headers
|
||||||
|
│ ├── c_utils.h # Main header (includes all utilities)
|
||||||
|
│ ├── debug.h # Debug/logging system
|
||||||
|
│ ├── version.h # Version utilities
|
||||||
|
│ ├── string_utils.h # String utilities (future)
|
||||||
|
│ └── memory_utils.h # Memory utilities (future)
|
||||||
|
│
|
||||||
|
├── src/ # Implementation files
|
||||||
|
│ ├── debug.c # Debug system implementation
|
||||||
|
│ ├── version.c # Version utilities implementation
|
||||||
|
│ ├── string_utils.c # String utilities (future)
|
||||||
|
│ └── memory_utils.c # Memory utilities (future)
|
||||||
|
│
|
||||||
|
├── examples/ # Usage examples
|
||||||
|
│ ├── debug_example.c # Debug system example
|
||||||
|
│ ├── version_example.c # Version utilities example
|
||||||
|
│ └── Makefile # Examples build system
|
||||||
|
│
|
||||||
|
├── tests/ # Unit tests
|
||||||
|
│ ├── test_debug.c # Debug system tests
|
||||||
|
│ ├── test_version.c # Version utilities tests
|
||||||
|
│ ├── run_tests.sh # Test runner
|
||||||
|
│ └── Makefile # Tests build system
|
||||||
|
│
|
||||||
|
└── docs/ # Additional documentation
|
||||||
|
├── API.md # Complete API reference
|
||||||
|
├── INTEGRATION.md # How to integrate into projects
|
||||||
|
├── VERSIONING.md # Versioning system guide
|
||||||
|
└── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Utilities (v0.1.0)
|
||||||
|
|
||||||
|
### 1. Debug System (`debug.h`, `debug.c`)
|
||||||
|
|
||||||
|
**Purpose**: Unified logging and debugging system with configurable verbosity levels.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- 5 debug levels: NONE, ERROR, WARN, INFO, DEBUG, TRACE
|
||||||
|
- Timestamp formatting
|
||||||
|
- File/line information at TRACE level
|
||||||
|
- Macro-based API for zero-cost when disabled
|
||||||
|
- Thread-safe (future enhancement)
|
||||||
|
|
||||||
|
**API**:
|
||||||
|
```c
|
||||||
|
// Initialization
|
||||||
|
void debug_init(int level);
|
||||||
|
|
||||||
|
// Logging macros
|
||||||
|
DEBUG_ERROR(format, ...);
|
||||||
|
DEBUG_WARN(format, ...);
|
||||||
|
DEBUG_INFO(format, ...);
|
||||||
|
DEBUG_LOG(format, ...);
|
||||||
|
DEBUG_TRACE(format, ...);
|
||||||
|
|
||||||
|
// Global debug level
|
||||||
|
extern debug_level_t g_debug_level;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example**:
|
||||||
|
```c
|
||||||
|
#include <c_utils/debug.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
debug_init(DEBUG_LEVEL_INFO);
|
||||||
|
DEBUG_INFO("Application started");
|
||||||
|
DEBUG_ERROR("Critical error: %s", error_msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Version Utilities (`version.h`, `version.c`)
|
||||||
|
|
||||||
|
**Purpose**: Reusable versioning system for C projects using git tags.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Automatic version extraction from git tags
|
||||||
|
- Semantic versioning support (MAJOR.MINOR.PATCH)
|
||||||
|
- Version comparison functions
|
||||||
|
- Header file generation for embedding version info
|
||||||
|
- Build number tracking
|
||||||
|
|
||||||
|
**API**:
|
||||||
|
```c
|
||||||
|
// Version structure
|
||||||
|
typedef struct {
|
||||||
|
int major;
|
||||||
|
int minor;
|
||||||
|
int patch;
|
||||||
|
char* git_hash;
|
||||||
|
char* build_date;
|
||||||
|
} version_info_t;
|
||||||
|
|
||||||
|
// Get version from git
|
||||||
|
int version_get_from_git(version_info_t* version);
|
||||||
|
|
||||||
|
// Generate version header file
|
||||||
|
int version_generate_header(const char* output_path, const char* prefix);
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
int version_compare(version_info_t* v1, version_info_t* v2);
|
||||||
|
|
||||||
|
// Format version string
|
||||||
|
char* version_to_string(version_info_t* version);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Example**:
|
||||||
|
```c
|
||||||
|
#include <c_utils/version.h>
|
||||||
|
|
||||||
|
// In your build system:
|
||||||
|
version_generate_header("src/version.h", "MY_APP");
|
||||||
|
|
||||||
|
// In your code:
|
||||||
|
#include "version.h"
|
||||||
|
printf("Version: %s\n", MY_APP_VERSION);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration with Projects**:
|
||||||
|
```bash
|
||||||
|
# In project Makefile
|
||||||
|
version.h:
|
||||||
|
c_utils_lib/bin/generate_version src/version.h MY_PROJECT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
### Static Library Output
|
||||||
|
|
||||||
|
```
|
||||||
|
libc_utils.a # Static library for linking
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Targets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make # Build static library
|
||||||
|
make examples # Build examples
|
||||||
|
make test # Run tests
|
||||||
|
make install # Install to system (optional)
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Script (`build.sh`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Simplified build script similar to nostr_core_lib
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
lib|"")
|
||||||
|
make
|
||||||
|
;;
|
||||||
|
examples)
|
||||||
|
make examples
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
make test
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
make clean
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
make install
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning System Design
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Git Tags as Source of Truth**
|
||||||
|
- Version tags: `v0.1.0`, `v0.2.0`, etc.
|
||||||
|
- Follows semantic versioning
|
||||||
|
|
||||||
|
2. **Automatic Header Generation**
|
||||||
|
- Script reads git tags
|
||||||
|
- Generates header with version macros
|
||||||
|
- Includes build date and git hash
|
||||||
|
|
||||||
|
3. **Reusable Across Projects**
|
||||||
|
- Each project calls `version_generate_header()`
|
||||||
|
- Customizable prefix (e.g., `C_RELAY_VERSION`, `NOSTR_CORE_VERSION`)
|
||||||
|
- No hardcoded version numbers in source
|
||||||
|
|
||||||
|
### Example Generated Header
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Auto-generated by c_utils_lib version system
|
||||||
|
#ifndef MY_PROJECT_VERSION_H
|
||||||
|
#define MY_PROJECT_VERSION_H
|
||||||
|
|
||||||
|
#define MY_PROJECT_VERSION "v0.1.0"
|
||||||
|
#define MY_PROJECT_VERSION_MAJOR 0
|
||||||
|
#define MY_PROJECT_VERSION_MINOR 1
|
||||||
|
#define MY_PROJECT_VERSION_PATCH 0
|
||||||
|
#define MY_PROJECT_GIT_HASH "a1b2c3d"
|
||||||
|
#define MY_PROJECT_BUILD_DATE "2025-10-15"
|
||||||
|
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Pattern
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# In consuming project's Makefile
|
||||||
|
VERSION_SCRIPT = c_utils_lib/bin/generate_version
|
||||||
|
|
||||||
|
src/version.h: .git/refs/tags/*
|
||||||
|
$(VERSION_SCRIPT) src/version.h MY_PROJECT
|
||||||
|
|
||||||
|
my_app: src/version.h src/main.c
|
||||||
|
$(CC) src/main.c -o my_app -Ic_utils_lib/include -Lc_utils_lib -lc_utils
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Utilities (Roadmap)
|
||||||
|
|
||||||
|
### String Utilities (`string_utils.h`)
|
||||||
|
- Safe string operations (bounds checking)
|
||||||
|
- String trimming, splitting, joining
|
||||||
|
- Case conversion
|
||||||
|
- Pattern matching helpers
|
||||||
|
|
||||||
|
### Memory Utilities (`memory_utils.h`)
|
||||||
|
- Safe allocation wrappers
|
||||||
|
- Memory pool management
|
||||||
|
- Leak detection helpers (debug builds)
|
||||||
|
- Arena allocators
|
||||||
|
|
||||||
|
### Configuration Utilities (`config_utils.h`)
|
||||||
|
- INI file parsing
|
||||||
|
- JSON configuration (using cJSON)
|
||||||
|
- Environment variable helpers
|
||||||
|
- Command-line argument parsing
|
||||||
|
|
||||||
|
### File Utilities (`file_utils.h`)
|
||||||
|
- Safe file operations
|
||||||
|
- Directory traversal
|
||||||
|
- Path manipulation
|
||||||
|
- File watching (inotify wrapper)
|
||||||
|
|
||||||
|
### Time Utilities (`time_utils.h`)
|
||||||
|
- Timestamp formatting
|
||||||
|
- Duration calculations
|
||||||
|
- Timer utilities
|
||||||
|
- Rate limiting helpers
|
||||||
|
|
||||||
|
## Integration Guide
|
||||||
|
|
||||||
|
### As Git Submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your project
|
||||||
|
git submodule add https://github.com/yourusername/c_utils_lib.git
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Build the library
|
||||||
|
cd c_utils_lib && ./build.sh lib && cd ..
|
||||||
|
|
||||||
|
# Update your Makefile
|
||||||
|
INCLUDES += -Ic_utils_lib/include
|
||||||
|
LIBS += -Lc_utils_lib -lc_utils
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Your Makefile
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Check if c_utils_lib is built
|
||||||
|
c_utils_lib/libc_utils.a:
|
||||||
|
cd c_utils_lib && ./build.sh lib
|
||||||
|
|
||||||
|
# Link against it
|
||||||
|
my_app: c_utils_lib/libc_utils.a src/main.c
|
||||||
|
$(CC) src/main.c -o my_app \
|
||||||
|
-Ic_utils_lib/include \
|
||||||
|
-Lc_utils_lib -lc_utils
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Your Code
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Option 1: Include everything
|
||||||
|
#include <c_utils/c_utils.h>
|
||||||
|
|
||||||
|
// Option 2: Include specific utilities
|
||||||
|
#include <c_utils/debug.h>
|
||||||
|
#include <c_utils/version.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
debug_init(DEBUG_LEVEL_INFO);
|
||||||
|
DEBUG_INFO("Starting application version %s", MY_APP_VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Plan for c-relay
|
||||||
|
|
||||||
|
### Phase 1: Extract Debug System
|
||||||
|
1. Create `c_utils_lib` repository
|
||||||
|
2. Move [`debug.c`](../src/debug.c) and [`debug.h`](../src/debug.h)
|
||||||
|
3. Create build system
|
||||||
|
4. Add basic tests
|
||||||
|
|
||||||
|
### Phase 2: Add Versioning System
|
||||||
|
1. Extract version generation logic from c-relay
|
||||||
|
2. Create reusable version utilities
|
||||||
|
3. Update c-relay to use new system
|
||||||
|
4. Update nostr_core_lib to use new system
|
||||||
|
|
||||||
|
### Phase 3: Add as Submodule
|
||||||
|
1. Add `c_utils_lib` as submodule to c-relay
|
||||||
|
2. Update c-relay Makefile
|
||||||
|
3. Update includes in c-relay source files
|
||||||
|
4. Remove old debug files from c-relay
|
||||||
|
|
||||||
|
### Phase 4: Documentation & Examples
|
||||||
|
1. Create comprehensive README
|
||||||
|
2. Add usage examples
|
||||||
|
3. Write integration guide
|
||||||
|
4. Document API
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### For c-relay
|
||||||
|
- Cleaner separation of concerns
|
||||||
|
- Reusable utilities across projects
|
||||||
|
- Easier to maintain and test
|
||||||
|
- Consistent logging across codebase
|
||||||
|
|
||||||
|
### For Learning C
|
||||||
|
- Real-world utility implementations
|
||||||
|
- Best practices examples
|
||||||
|
- Modular design patterns
|
||||||
|
- Build system examples
|
||||||
|
|
||||||
|
### For Future Projects
|
||||||
|
- Drop-in utility library
|
||||||
|
- Proven, tested code
|
||||||
|
- Consistent patterns
|
||||||
|
- Time savings
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Test each utility independently
|
||||||
|
- Mock external dependencies
|
||||||
|
- Edge case coverage
|
||||||
|
- Memory leak detection (valgrind)
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Test with real projects (c-relay, nostr_core_lib)
|
||||||
|
- Cross-platform testing
|
||||||
|
- Performance benchmarks
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
- GitHub Actions for automated testing
|
||||||
|
- Multiple compiler versions (gcc, clang)
|
||||||
|
- Multiple platforms (Linux, macOS)
|
||||||
|
- Static analysis (cppcheck, clang-tidy)
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
### Code Documentation
|
||||||
|
- Doxygen-style comments
|
||||||
|
- Function purpose and parameters
|
||||||
|
- Return value descriptions
|
||||||
|
- Usage examples in comments
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- Complete API reference in `docs/API.md`
|
||||||
|
- Usage examples for each function
|
||||||
|
- Common patterns and best practices
|
||||||
|
- Migration guides
|
||||||
|
|
||||||
|
### Learning Resources
|
||||||
|
- Detailed explanations of implementations
|
||||||
|
- Links to relevant C standards
|
||||||
|
- Common pitfalls and how to avoid them
|
||||||
|
- Performance considerations
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - permissive and suitable for learning and commercial use.
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v0.1.0** (Planned)
|
||||||
|
- Initial release
|
||||||
|
- Debug system
|
||||||
|
- Version utilities
|
||||||
|
- Basic documentation
|
||||||
|
|
||||||
|
- **v0.2.0** (Future)
|
||||||
|
- String utilities
|
||||||
|
- Memory utilities
|
||||||
|
- Enhanced documentation
|
||||||
|
|
||||||
|
- **v0.3.0** (Future)
|
||||||
|
- Configuration utilities
|
||||||
|
- File utilities
|
||||||
|
- Time utilities
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. ✅ Successfully integrated into c-relay
|
||||||
|
2. ✅ Successfully integrated into nostr_core_lib
|
||||||
|
3. ✅ All tests passing
|
||||||
|
4. ✅ Documentation complete
|
||||||
|
5. ✅ Examples working
|
||||||
|
6. ✅ Zero external dependencies (except standard library)
|
||||||
|
7. ✅ Cross-platform compatibility verified
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Create repository structure
|
||||||
|
2. Implement debug system
|
||||||
|
3. Implement version utilities
|
||||||
|
4. Create build system
|
||||||
|
5. Write tests
|
||||||
|
6. Create documentation
|
||||||
|
7. Integrate into c-relay
|
||||||
|
8. Publish to GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This is a living document. Update as the library evolves and new utilities are added.
|
||||||
621
docs/c_utils_lib_implementation_plan.md
Normal file
621
docs/c_utils_lib_implementation_plan.md
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
# c_utils_lib Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document provides a step-by-step implementation plan for creating the `c_utils_lib` library and integrating it into the c-relay project.
|
||||||
|
|
||||||
|
## Phase 1: Repository Setup & Structure
|
||||||
|
|
||||||
|
### Step 1.1: Create Repository Structure
|
||||||
|
|
||||||
|
**Location**: Create outside c-relay project (sibling directory)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create directory structure
|
||||||
|
mkdir -p c_utils_lib/{include,src,examples,tests,docs,bin}
|
||||||
|
cd c_utils_lib
|
||||||
|
|
||||||
|
# Create subdirectories
|
||||||
|
mkdir -p include/c_utils
|
||||||
|
mkdir -p tests/results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.2: Initialize Git Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd c_utils_lib
|
||||||
|
git init
|
||||||
|
git branch -M main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1.3: Create Core Files
|
||||||
|
|
||||||
|
**Files to create**:
|
||||||
|
1. `README.md` - Main documentation
|
||||||
|
2. `LICENSE` - MIT License
|
||||||
|
3. `VERSION` - Version file (v0.1.0)
|
||||||
|
4. `.gitignore` - Git ignore rules
|
||||||
|
5. `Makefile` - Build system
|
||||||
|
6. `build.sh` - Build script
|
||||||
|
|
||||||
|
## Phase 2: Debug System Implementation
|
||||||
|
|
||||||
|
### Step 2.1: Move Debug Files
|
||||||
|
|
||||||
|
**Source files** (from c-relay):
|
||||||
|
- `src/debug.c` → `c_utils_lib/src/debug.c`
|
||||||
|
- `src/debug.h` → `c_utils_lib/include/c_utils/debug.h`
|
||||||
|
|
||||||
|
**Modifications needed**:
|
||||||
|
1. Update header guard in `debug.h`:
|
||||||
|
```c
|
||||||
|
#ifndef C_UTILS_DEBUG_H
|
||||||
|
#define C_UTILS_DEBUG_H
|
||||||
|
```
|
||||||
|
|
||||||
|
2. No namespace changes needed (keep simple API)
|
||||||
|
|
||||||
|
3. Add header documentation:
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* @file debug.h
|
||||||
|
* @brief Debug and logging system with configurable verbosity levels
|
||||||
|
*
|
||||||
|
* Provides a simple, efficient logging system with 5 levels:
|
||||||
|
* - ERROR: Critical errors
|
||||||
|
* - WARN: Warnings
|
||||||
|
* - INFO: Informational messages
|
||||||
|
* - DEBUG: Debug messages
|
||||||
|
* - TRACE: Detailed trace with file:line info
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2.2: Create Main Header
|
||||||
|
|
||||||
|
**File**: `include/c_utils/c_utils.h`
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef C_UTILS_H
|
||||||
|
#define C_UTILS_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file c_utils.h
|
||||||
|
* @brief Main header for c_utils_lib - includes all utilities
|
||||||
|
*
|
||||||
|
* Include this header to access all c_utils_lib functionality.
|
||||||
|
* Alternatively, include specific headers for modular usage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Version information
|
||||||
|
#define C_UTILS_VERSION "v0.1.0"
|
||||||
|
#define C_UTILS_VERSION_MAJOR 0
|
||||||
|
#define C_UTILS_VERSION_MINOR 1
|
||||||
|
#define C_UTILS_VERSION_PATCH 0
|
||||||
|
|
||||||
|
// Include all utilities
|
||||||
|
#include "debug.h"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
|
#endif /* C_UTILS_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Version Utilities Implementation
|
||||||
|
|
||||||
|
### Step 3.1: Design Version API
|
||||||
|
|
||||||
|
**File**: `include/c_utils/version.h`
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef C_UTILS_VERSION_H
|
||||||
|
#define C_UTILS_VERSION_H
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Version information structure
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
int major;
|
||||||
|
int minor;
|
||||||
|
int patch;
|
||||||
|
char git_hash[41]; // SHA-1 hash (40 chars + null)
|
||||||
|
char build_date[32]; // ISO 8601 format
|
||||||
|
char version_string[64]; // "vX.Y.Z" format
|
||||||
|
} version_info_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Extract version from git tags
|
||||||
|
* @param version Output version structure
|
||||||
|
* @return 0 on success, -1 on error
|
||||||
|
*/
|
||||||
|
int version_get_from_git(version_info_t* version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate version header file for a project
|
||||||
|
* @param output_path Path to output header file
|
||||||
|
* @param prefix Prefix for macros (e.g., "MY_APP")
|
||||||
|
* @return 0 on success, -1 on error
|
||||||
|
*/
|
||||||
|
int version_generate_header(const char* output_path, const char* prefix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compare two versions
|
||||||
|
* @return -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
||||||
|
*/
|
||||||
|
int version_compare(const version_info_t* v1, const version_info_t* v2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format version as string
|
||||||
|
* @param version Version structure
|
||||||
|
* @param buffer Output buffer
|
||||||
|
* @param buffer_size Size of output buffer
|
||||||
|
* @return Number of characters written
|
||||||
|
*/
|
||||||
|
int version_to_string(const version_info_t* version, char* buffer, size_t buffer_size);
|
||||||
|
|
||||||
|
#endif /* C_UTILS_VERSION_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3.2: Implement Version Utilities
|
||||||
|
|
||||||
|
**File**: `src/version.c`
|
||||||
|
|
||||||
|
Key functions to implement:
|
||||||
|
1. `version_get_from_git()` - Execute `git describe --tags` and parse
|
||||||
|
2. `version_generate_header()` - Generate header file with macros
|
||||||
|
3. `version_compare()` - Semantic version comparison
|
||||||
|
4. `version_to_string()` - Format version string
|
||||||
|
|
||||||
|
### Step 3.3: Create Version Generation Script
|
||||||
|
|
||||||
|
**File**: `bin/generate_version`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Generate version header for a project
|
||||||
|
|
||||||
|
OUTPUT_FILE="$1"
|
||||||
|
PREFIX="$2"
|
||||||
|
|
||||||
|
if [ -z "$OUTPUT_FILE" ] || [ -z "$PREFIX" ]; then
|
||||||
|
echo "Usage: $0 <output_file> <prefix>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get version from git
|
||||||
|
if [ -d .git ]; then
|
||||||
|
VERSION=$(git describe --tags --always 2>/dev/null || echo "v0.0.0")
|
||||||
|
GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
else
|
||||||
|
VERSION="v0.0.0"
|
||||||
|
GIT_HASH="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse version
|
||||||
|
CLEAN_VERSION=$(echo "$VERSION" | sed 's/^v//' | cut -d- -f1)
|
||||||
|
MAJOR=$(echo "$CLEAN_VERSION" | cut -d. -f1)
|
||||||
|
MINOR=$(echo "$CLEAN_VERSION" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$CLEAN_VERSION" | cut -d. -f3)
|
||||||
|
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
# Generate header
|
||||||
|
cat > "$OUTPUT_FILE" << EOF
|
||||||
|
/* Auto-generated by c_utils_lib version system */
|
||||||
|
/* DO NOT EDIT - This file is automatically generated */
|
||||||
|
|
||||||
|
#ifndef ${PREFIX}_VERSION_H
|
||||||
|
#define ${PREFIX}_VERSION_H
|
||||||
|
|
||||||
|
#define ${PREFIX}_VERSION "v${CLEAN_VERSION}"
|
||||||
|
#define ${PREFIX}_VERSION_MAJOR ${MAJOR}
|
||||||
|
#define ${PREFIX}_VERSION_MINOR ${MINOR}
|
||||||
|
#define ${PREFIX}_VERSION_PATCH ${PATCH}
|
||||||
|
#define ${PREFIX}_GIT_HASH "${GIT_HASH}"
|
||||||
|
#define ${PREFIX}_BUILD_DATE "${BUILD_DATE}"
|
||||||
|
|
||||||
|
#endif /* ${PREFIX}_VERSION_H */
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Generated $OUTPUT_FILE with version v${CLEAN_VERSION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: Build System
|
||||||
|
|
||||||
|
### Step 4.1: Create Makefile
|
||||||
|
|
||||||
|
**File**: `Makefile`
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# c_utils_lib Makefile
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
AR = ar
|
||||||
|
CFLAGS = -Wall -Wextra -std=c99 -O2 -g
|
||||||
|
INCLUDES = -Iinclude
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
SRC_DIR = src
|
||||||
|
INCLUDE_DIR = include
|
||||||
|
BUILD_DIR = build
|
||||||
|
EXAMPLES_DIR = examples
|
||||||
|
TESTS_DIR = tests
|
||||||
|
|
||||||
|
# Source files
|
||||||
|
SOURCES = $(wildcard $(SRC_DIR)/*.c)
|
||||||
|
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
|
||||||
|
|
||||||
|
# Output library
|
||||||
|
LIBRARY = libc_utils.a
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: $(LIBRARY)
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
$(BUILD_DIR):
|
||||||
|
mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
|
# Compile source files
|
||||||
|
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||||
|
|
||||||
|
# Create static library
|
||||||
|
$(LIBRARY): $(OBJECTS)
|
||||||
|
$(AR) rcs $@ $^
|
||||||
|
@echo "Built $(LIBRARY)"
|
||||||
|
|
||||||
|
# Build examples
|
||||||
|
examples: $(LIBRARY)
|
||||||
|
$(MAKE) -C $(EXAMPLES_DIR)
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test: $(LIBRARY)
|
||||||
|
$(MAKE) -C $(TESTS_DIR)
|
||||||
|
$(TESTS_DIR)/run_tests.sh
|
||||||
|
|
||||||
|
# Install to system (optional)
|
||||||
|
install: $(LIBRARY)
|
||||||
|
install -d /usr/local/lib
|
||||||
|
install -m 644 $(LIBRARY) /usr/local/lib/
|
||||||
|
install -d /usr/local/include/c_utils
|
||||||
|
install -m 644 $(INCLUDE_DIR)/c_utils/*.h /usr/local/include/c_utils/
|
||||||
|
@echo "Installed to /usr/local"
|
||||||
|
|
||||||
|
# Uninstall from system
|
||||||
|
uninstall:
|
||||||
|
rm -f /usr/local/lib/$(LIBRARY)
|
||||||
|
rm -rf /usr/local/include/c_utils
|
||||||
|
@echo "Uninstalled from /usr/local"
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR) $(LIBRARY)
|
||||||
|
$(MAKE) -C $(EXAMPLES_DIR) clean 2>/dev/null || true
|
||||||
|
$(MAKE) -C $(TESTS_DIR) clean 2>/dev/null || true
|
||||||
|
|
||||||
|
# Help
|
||||||
|
help:
|
||||||
|
@echo "c_utils_lib Build System"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " all Build static library (default)"
|
||||||
|
@echo " examples Build examples"
|
||||||
|
@echo " test Run tests"
|
||||||
|
@echo " install Install to /usr/local"
|
||||||
|
@echo " uninstall Remove from /usr/local"
|
||||||
|
@echo " clean Clean build artifacts"
|
||||||
|
@echo " help Show this help"
|
||||||
|
|
||||||
|
.PHONY: all examples test install uninstall clean help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4.2: Create Build Script
|
||||||
|
|
||||||
|
**File**: `build.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# c_utils_lib build script
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
lib|"")
|
||||||
|
echo "Building c_utils_lib..."
|
||||||
|
make
|
||||||
|
;;
|
||||||
|
examples)
|
||||||
|
echo "Building examples..."
|
||||||
|
make examples
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
echo "Running tests..."
|
||||||
|
make test
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
echo "Cleaning..."
|
||||||
|
make clean
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
echo "Installing..."
|
||||||
|
make install
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5: Examples & Tests
|
||||||
|
|
||||||
|
### Step 5.1: Create Debug Example
|
||||||
|
|
||||||
|
**File**: `examples/debug_example.c`
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <c_utils/debug.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Initialize with INFO level
|
||||||
|
debug_init(DEBUG_LEVEL_INFO);
|
||||||
|
|
||||||
|
DEBUG_INFO("Application started");
|
||||||
|
DEBUG_WARN("This is a warning");
|
||||||
|
DEBUG_ERROR("This is an error");
|
||||||
|
|
||||||
|
// This won't print (level too high)
|
||||||
|
DEBUG_LOG("This debug message won't show");
|
||||||
|
|
||||||
|
// Change level to DEBUG
|
||||||
|
g_debug_level = DEBUG_LEVEL_DEBUG;
|
||||||
|
DEBUG_LOG("Now debug messages show");
|
||||||
|
|
||||||
|
// Change to TRACE to see file:line info
|
||||||
|
g_debug_level = DEBUG_LEVEL_TRACE;
|
||||||
|
DEBUG_TRACE("Trace with file:line information");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5.2: Create Version Example
|
||||||
|
|
||||||
|
**File**: `examples/version_example.c`
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <c_utils/version.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
version_info_t version;
|
||||||
|
|
||||||
|
// Get version from git
|
||||||
|
if (version_get_from_git(&version) == 0) {
|
||||||
|
char version_str[64];
|
||||||
|
version_to_string(&version, version_str, sizeof(version_str));
|
||||||
|
|
||||||
|
printf("Version: %s\n", version_str);
|
||||||
|
printf("Git Hash: %s\n", version.git_hash);
|
||||||
|
printf("Build Date: %s\n", version.build_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5.3: Create Test Suite
|
||||||
|
|
||||||
|
**File**: `tests/test_debug.c`
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <c_utils/debug.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int test_debug_init() {
|
||||||
|
debug_init(DEBUG_LEVEL_INFO);
|
||||||
|
return (g_debug_level == DEBUG_LEVEL_INFO) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_debug_levels() {
|
||||||
|
// Test that higher levels don't print at lower settings
|
||||||
|
debug_init(DEBUG_LEVEL_ERROR);
|
||||||
|
// Would need to capture stdout to verify
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int failed = 0;
|
||||||
|
|
||||||
|
printf("Running debug tests...\n");
|
||||||
|
|
||||||
|
if (test_debug_init() != 0) {
|
||||||
|
printf("FAIL: test_debug_init\n");
|
||||||
|
failed++;
|
||||||
|
} else {
|
||||||
|
printf("PASS: test_debug_init\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test_debug_levels() != 0) {
|
||||||
|
printf("FAIL: test_debug_levels\n");
|
||||||
|
failed++;
|
||||||
|
} else {
|
||||||
|
printf("PASS: test_debug_levels\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Documentation
|
||||||
|
|
||||||
|
### Step 6.1: Create README.md
|
||||||
|
|
||||||
|
Key sections:
|
||||||
|
1. Overview and purpose
|
||||||
|
2. Quick start guide
|
||||||
|
3. Installation instructions
|
||||||
|
4. Usage examples
|
||||||
|
5. API reference (brief)
|
||||||
|
6. Integration guide
|
||||||
|
7. Contributing guidelines
|
||||||
|
8. License
|
||||||
|
|
||||||
|
### Step 6.2: Create API Documentation
|
||||||
|
|
||||||
|
**File**: `docs/API.md`
|
||||||
|
|
||||||
|
Complete API reference with:
|
||||||
|
- Function signatures
|
||||||
|
- Parameter descriptions
|
||||||
|
- Return values
|
||||||
|
- Usage examples
|
||||||
|
- Common patterns
|
||||||
|
|
||||||
|
### Step 6.3: Create Integration Guide
|
||||||
|
|
||||||
|
**File**: `docs/INTEGRATION.md`
|
||||||
|
|
||||||
|
How to integrate into projects:
|
||||||
|
1. As git submodule
|
||||||
|
2. Makefile integration
|
||||||
|
3. Code examples
|
||||||
|
4. Migration from standalone utilities
|
||||||
|
|
||||||
|
## Phase 7: Integration with c-relay
|
||||||
|
|
||||||
|
### Step 7.1: Add as Submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/c-relay
|
||||||
|
git submodule add <repo-url> c_utils_lib
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7.2: Update c-relay Makefile
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Add to c-relay Makefile
|
||||||
|
C_UTILS_LIB = c_utils_lib/libc_utils.a
|
||||||
|
|
||||||
|
# Update includes
|
||||||
|
INCLUDES += -Ic_utils_lib/include
|
||||||
|
|
||||||
|
# Update libs
|
||||||
|
LIBS += -Lc_utils_lib -lc_utils
|
||||||
|
|
||||||
|
# Add dependency
|
||||||
|
$(C_UTILS_LIB):
|
||||||
|
cd c_utils_lib && ./build.sh lib
|
||||||
|
|
||||||
|
# Update main target
|
||||||
|
$(TARGET): $(C_UTILS_LIB) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7.3: Update c-relay Source Files
|
||||||
|
|
||||||
|
**Changes needed**:
|
||||||
|
|
||||||
|
1. Update includes:
|
||||||
|
```c
|
||||||
|
// Old
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
// New
|
||||||
|
#include <c_utils/debug.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Remove old debug files:
|
||||||
|
```bash
|
||||||
|
git rm src/debug.c src/debug.h
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update all files that use debug system:
|
||||||
|
- `src/main.c`
|
||||||
|
- `src/config.c`
|
||||||
|
- `src/dm_admin.c`
|
||||||
|
- `src/websockets.c`
|
||||||
|
- `src/subscriptions.c`
|
||||||
|
- Any other files using DEBUG_* macros
|
||||||
|
|
||||||
|
### Step 7.4: Test Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd c-relay
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
./make_and_restart_relay.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
- Compilation succeeds
|
||||||
|
- Debug output works correctly
|
||||||
|
- No functionality regressions
|
||||||
|
|
||||||
|
## Phase 8: Version System Integration
|
||||||
|
|
||||||
|
### Step 8.1: Update c-relay Makefile for Versioning
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Add version generation
|
||||||
|
src/version.h: .git/refs/tags/*
|
||||||
|
c_utils_lib/bin/generate_version src/version.h C_RELAY
|
||||||
|
|
||||||
|
# Add dependency
|
||||||
|
$(TARGET): src/version.h ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8.2: Update c-relay to Use Generated Version
|
||||||
|
|
||||||
|
Replace hardcoded version in `src/main.h` with:
|
||||||
|
```c
|
||||||
|
#include "version.h"
|
||||||
|
// Use C_RELAY_VERSION instead of hardcoded VERSION
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeline Estimate
|
||||||
|
|
||||||
|
- **Phase 1**: Repository Setup - 1 hour
|
||||||
|
- **Phase 2**: Debug System - 2 hours
|
||||||
|
- **Phase 3**: Version Utilities - 4 hours
|
||||||
|
- **Phase 4**: Build System - 2 hours
|
||||||
|
- **Phase 5**: Examples & Tests - 3 hours
|
||||||
|
- **Phase 6**: Documentation - 3 hours
|
||||||
|
- **Phase 7**: c-relay Integration - 2 hours
|
||||||
|
- **Phase 8**: Version Integration - 2 hours
|
||||||
|
|
||||||
|
**Total**: ~19 hours
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] c_utils_lib builds successfully
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Examples compile and run
|
||||||
|
- [ ] c-relay integrates successfully
|
||||||
|
- [ ] Debug output works in c-relay
|
||||||
|
- [ ] Version generation works
|
||||||
|
- [ ] Documentation complete
|
||||||
|
- [ ] No regressions in c-relay functionality
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Review this plan with stakeholders
|
||||||
|
2. Create repository structure
|
||||||
|
3. Implement debug system
|
||||||
|
4. Implement version utilities
|
||||||
|
5. Create build system
|
||||||
|
6. Write tests and examples
|
||||||
|
7. Create documentation
|
||||||
|
8. Integrate into c-relay
|
||||||
|
9. Test thoroughly
|
||||||
|
10. Publish to GitHub
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep the API simple and intuitive
|
||||||
|
- Focus on zero external dependencies
|
||||||
|
- Prioritize learning value in code comments
|
||||||
|
- Make integration as easy as possible
|
||||||
|
- Document everything thoroughly
|
||||||
562
docs/debug_system.md
Normal file
562
docs/debug_system.md
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
# Simple Debug System Proposal
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A minimal debug system with 6 levels (0-5) controlled by a single `--debug-level` flag. TRACE level (5) automatically includes file:line information for ALL messages. Uses compile-time macros to ensure **zero performance impact and zero size increase** in production builds.
|
||||||
|
|
||||||
|
## Debug Levels
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
DEBUG_LEVEL_NONE = 0, // Production: no debug output
|
||||||
|
DEBUG_LEVEL_ERROR = 1, // Errors only
|
||||||
|
DEBUG_LEVEL_WARN = 2, // Errors + Warnings
|
||||||
|
DEBUG_LEVEL_INFO = 3, // Errors + Warnings + Info
|
||||||
|
DEBUG_LEVEL_DEBUG = 4, // All above + Debug messages
|
||||||
|
DEBUG_LEVEL_TRACE = 5 // All above + Trace (very verbose)
|
||||||
|
} debug_level_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production (default - no debug output)
|
||||||
|
./c_relay_x86
|
||||||
|
|
||||||
|
# Show errors only
|
||||||
|
./c_relay_x86 --debug-level=1
|
||||||
|
|
||||||
|
# Show errors and warnings
|
||||||
|
./c_relay_x86 --debug-level=2
|
||||||
|
|
||||||
|
# Show errors, warnings, and info (recommended for development)
|
||||||
|
./c_relay_x86 --debug-level=3
|
||||||
|
|
||||||
|
# Show all debug messages
|
||||||
|
./c_relay_x86 --debug-level=4
|
||||||
|
|
||||||
|
# Show everything including trace with file:line (very verbose)
|
||||||
|
./c_relay_x86 --debug-level=5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### 1. Header File (`src/debug.h`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef DEBUG_H
|
||||||
|
#define DEBUG_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Debug levels
|
||||||
|
typedef enum {
|
||||||
|
DEBUG_LEVEL_NONE = 0,
|
||||||
|
DEBUG_LEVEL_ERROR = 1,
|
||||||
|
DEBUG_LEVEL_WARN = 2,
|
||||||
|
DEBUG_LEVEL_INFO = 3,
|
||||||
|
DEBUG_LEVEL_DEBUG = 4,
|
||||||
|
DEBUG_LEVEL_TRACE = 5
|
||||||
|
} debug_level_t;
|
||||||
|
|
||||||
|
// Global debug level (set at runtime via CLI)
|
||||||
|
extern debug_level_t g_debug_level;
|
||||||
|
|
||||||
|
// Initialize debug system
|
||||||
|
void debug_init(int level);
|
||||||
|
|
||||||
|
// Core logging function
|
||||||
|
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
|
||||||
|
|
||||||
|
// Convenience macros that check level before calling
|
||||||
|
// Note: TRACE level (5) and above include file:line information for ALL messages
|
||||||
|
#define DEBUG_ERROR(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
#define DEBUG_WARN(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
#define DEBUG_INFO(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
#define DEBUG_LOG(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
#define DEBUG_TRACE(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
#endif /* DEBUG_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Implementation File (`src/debug.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "debug.h"
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// Global debug level (default: no debug output)
|
||||||
|
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
|
||||||
|
|
||||||
|
void debug_init(int level) {
|
||||||
|
if (level < 0) level = 0;
|
||||||
|
if (level > 5) level = 5;
|
||||||
|
g_debug_level = (debug_level_t)level;
|
||||||
|
}
|
||||||
|
|
||||||
|
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
|
||||||
|
// Get timestamp
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm* tm_info = localtime(&now);
|
||||||
|
char timestamp[32];
|
||||||
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
|
||||||
|
|
||||||
|
// Get level string
|
||||||
|
const char* level_str = "UNKNOWN";
|
||||||
|
switch (level) {
|
||||||
|
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
|
||||||
|
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
|
||||||
|
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
|
||||||
|
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
|
||||||
|
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print prefix with timestamp and level
|
||||||
|
printf("[%s] [%s] ", timestamp, level_str);
|
||||||
|
|
||||||
|
// Print source location when debug level is TRACE (5) or higher
|
||||||
|
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
|
||||||
|
// Extract just the filename (not full path)
|
||||||
|
const char* filename = strrchr(file, '/');
|
||||||
|
filename = filename ? filename + 1 : file;
|
||||||
|
printf("[%s:%d] ", filename, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print message
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vprintf(format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
printf("\n");
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CLI Argument Parsing (add to `src/main.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In main() function, add to argument parsing:
|
||||||
|
|
||||||
|
int debug_level = 0; // Default: no debug output
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strncmp(argv[i], "--debug-level=", 14) == 0) {
|
||||||
|
debug_level = atoi(argv[i] + 14);
|
||||||
|
if (debug_level < 0) debug_level = 0;
|
||||||
|
if (debug_level > 5) debug_level = 5;
|
||||||
|
}
|
||||||
|
// ... other arguments ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize debug system
|
||||||
|
debug_init(debug_level);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update Makefile
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Add debug.c to source files
|
||||||
|
MAIN_SRC = src/main.c src/config.c src/debug.c src/dm_admin.c src/request_validator.c ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Keep Existing Functions
|
||||||
|
|
||||||
|
The existing `log_*` functions can remain as wrappers:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// src/main.c - Update existing functions
|
||||||
|
// Note: These don't include file:line since they're wrappers
|
||||||
|
void log_info(const char* message) {
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
||||||
|
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "%s", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_error(const char* message) {
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_ERROR) {
|
||||||
|
debug_log(DEBUG_LEVEL_ERROR, NULL, 0, "%s", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_warning(const char* message) {
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_WARN) {
|
||||||
|
debug_log(DEBUG_LEVEL_WARN, NULL, 0, "%s", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_success(const char* message) {
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
||||||
|
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "✓ %s", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gradual Migration
|
||||||
|
|
||||||
|
Gradually replace log calls with debug macros:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Before:
|
||||||
|
log_info("Starting WebSocket relay server");
|
||||||
|
|
||||||
|
// After:
|
||||||
|
DEBUG_INFO("Starting WebSocket relay server");
|
||||||
|
|
||||||
|
// Before:
|
||||||
|
log_error("Failed to initialize database");
|
||||||
|
|
||||||
|
// After:
|
||||||
|
DEBUG_ERROR("Failed to initialize database");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add New Debug Levels
|
||||||
|
|
||||||
|
Add debug and trace messages where needed:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Detailed debugging
|
||||||
|
DEBUG_LOG("Processing subscription: %s", sub_id);
|
||||||
|
DEBUG_LOG("Filter count: %d", filter_count);
|
||||||
|
|
||||||
|
// Very verbose tracing
|
||||||
|
DEBUG_TRACE("Entering handle_req_message()");
|
||||||
|
DEBUG_TRACE("Subscription ID validated: %s", sub_id);
|
||||||
|
DEBUG_TRACE("Exiting handle_req_message()");
|
||||||
|
```
|
||||||
|
## Manual Guards for Expensive Operations
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
Debug macros use **runtime checks**, which means function arguments are always evaluated:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// ❌ BAD: Database query executes even when debug level is 0
|
||||||
|
DEBUG_LOG("Count: %d", expensive_database_query());
|
||||||
|
```
|
||||||
|
|
||||||
|
The `expensive_database_query()` will **always execute** because function arguments are evaluated before the `if` check inside the macro.
|
||||||
|
|
||||||
|
### The Solution: Manual Guards
|
||||||
|
|
||||||
|
For expensive operations (database queries, file I/O, complex calculations), use manual guards:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// ✅ GOOD: Query only executes when debugging is enabled
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||||
|
int count = expensive_database_query();
|
||||||
|
DEBUG_LOG("Count: %d", count);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standardized Comment Format
|
||||||
|
|
||||||
|
To make temporary debug guards easy to find and remove, use this standardized format:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// DEBUG_GUARD_START
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||||
|
// Expensive operation here
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
const char* sql = "SELECT COUNT(*) FROM events";
|
||||||
|
int count = 0;
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
count = sqlite3_column_int(stmt, 0);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
DEBUG_LOG("Event count: %d", count);
|
||||||
|
}
|
||||||
|
// DEBUG_GUARD_END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Easy Removal
|
||||||
|
|
||||||
|
When you're done debugging, find and remove all temporary guards:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all debug guards
|
||||||
|
grep -n "DEBUG_GUARD_START" src/*.c
|
||||||
|
|
||||||
|
# Remove guards with sed (between START and END markers)
|
||||||
|
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' src/config.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a simple script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# remove_debug_guards.sh
|
||||||
|
for file in src/*.c; do
|
||||||
|
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' "$file"
|
||||||
|
echo "Removed debug guards from $file"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Manual Guards
|
||||||
|
|
||||||
|
Use manual guards for:
|
||||||
|
- ✅ Database queries
|
||||||
|
- ✅ File I/O operations
|
||||||
|
- ✅ Network requests
|
||||||
|
- ✅ Complex calculations
|
||||||
|
- ✅ Memory allocations for debug data
|
||||||
|
- ✅ String formatting with multiple operations
|
||||||
|
|
||||||
|
Don't need guards for:
|
||||||
|
- ❌ Simple variable access
|
||||||
|
- ❌ Basic arithmetic
|
||||||
|
- ❌ String literals
|
||||||
|
- ❌ Function calls that are already cheap
|
||||||
|
|
||||||
|
### Example: Database Query Guard
|
||||||
|
|
||||||
|
```c
|
||||||
|
// DEBUG_GUARD_START
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||||
|
sqlite3_stmt* count_stmt;
|
||||||
|
const char* count_sql = "SELECT COUNT(*) FROM config";
|
||||||
|
int config_count = 0;
|
||||||
|
|
||||||
|
if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) {
|
||||||
|
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
|
||||||
|
config_count = sqlite3_column_int(count_stmt, 0);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(count_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG("Config table has %d rows", config_count);
|
||||||
|
}
|
||||||
|
// DEBUG_GUARD_END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Complex String Formatting Guard
|
||||||
|
|
||||||
|
```c
|
||||||
|
// DEBUG_GUARD_START
|
||||||
|
if (g_debug_level >= DEBUG_LEVEL_TRACE) {
|
||||||
|
char filter_str[1024] = {0};
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < filter_count && offset < sizeof(filter_str) - 1; i++) {
|
||||||
|
offset += snprintf(filter_str + offset, sizeof(filter_str) - offset,
|
||||||
|
"Filter %d: kind=%d, author=%s; ",
|
||||||
|
i, filters[i].kind, filters[i].author);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("Processing filters: %s", filter_str);
|
||||||
|
}
|
||||||
|
// DEBUG_GUARD_END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative: Compile-Time Guards
|
||||||
|
|
||||||
|
For permanent debug code that should be completely removed in production builds, use compile-time guards:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifdef ENABLE_DEBUG_CODE
|
||||||
|
// This code is completely removed when ENABLE_DEBUG_CODE is not defined
|
||||||
|
int count = expensive_database_query();
|
||||||
|
DEBUG_LOG("Count: %d", count);
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Build with debug code:
|
||||||
|
```bash
|
||||||
|
make CFLAGS="-DENABLE_DEBUG_CODE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Build without debug code (production):
|
||||||
|
```bash
|
||||||
|
make # No debug code compiled in
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Always use standardized markers** (`DEBUG_GUARD_START`/`DEBUG_GUARD_END`) for temporary guards
|
||||||
|
2. **Add a comment** explaining what you're debugging
|
||||||
|
3. **Remove guards** when debugging is complete
|
||||||
|
4. **Use compile-time guards** for permanent debug infrastructure
|
||||||
|
5. **Keep guards simple** - one guard per logical debug operation
|
||||||
|
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
### Runtime Check
|
||||||
|
|
||||||
|
The macros include a runtime check:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define DEBUG_INFO(...) \
|
||||||
|
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, NULL, 0, __VA_ARGS__); } while(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cost**: One integer comparison per debug statement (~1 CPU cycle)
|
||||||
|
|
||||||
|
**Impact**: Negligible - the comparison is faster than a function call
|
||||||
|
|
||||||
|
**Note**: Only `DEBUG_TRACE` includes `__FILE__` and `__LINE__`, which are compile-time constants with no runtime overhead.
|
||||||
|
|
||||||
|
### When Debug Level is 0 (Production)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// With g_debug_level = 0:
|
||||||
|
DEBUG_INFO("Starting server");
|
||||||
|
|
||||||
|
// Becomes:
|
||||||
|
if (0 >= 3) debug_log(...); // Never executes
|
||||||
|
|
||||||
|
// Compiler optimizes to:
|
||||||
|
// (nothing - branch is eliminated)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: Modern compilers (gcc -O2 or higher) will completely eliminate the dead code branch.
|
||||||
|
|
||||||
|
### Size Impact
|
||||||
|
|
||||||
|
**Test Case**: 100 debug statements in code
|
||||||
|
|
||||||
|
**Without optimization** (`-O0`):
|
||||||
|
- Binary size increase: ~2KB (branch instructions)
|
||||||
|
- Runtime cost: 100 comparisons per execution
|
||||||
|
|
||||||
|
**With optimization** (`-O2` or `-O3`):
|
||||||
|
- Binary size increase: **0 bytes** (dead code eliminated when g_debug_level = 0)
|
||||||
|
- Runtime cost: **0 cycles** (branches removed by compiler)
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
You can verify the optimization with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile with optimization
|
||||||
|
gcc -O2 -c debug_test.c -o debug_test.o
|
||||||
|
|
||||||
|
# Disassemble and check
|
||||||
|
objdump -d debug_test.o | grep -A 10 "debug_log"
|
||||||
|
```
|
||||||
|
|
||||||
|
When `g_debug_level = 0` (constant), you'll see the compiler has removed all debug calls.
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
### Level 0 (Production)
|
||||||
|
```
|
||||||
|
(no output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 1 (Errors Only)
|
||||||
|
```
|
||||||
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||||
|
[2025-01-12 14:30:20] [ERROR] WebSocket connection failed: port in use
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 2 (Errors + Warnings)
|
||||||
|
```
|
||||||
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||||
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||||
|
[2025-01-12 14:30:17] [WARN ] Configuration key 'relay_name' not found, using default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 3 (Errors + Warnings + Info)
|
||||||
|
```
|
||||||
|
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
||||||
|
[2025-01-12 14:30:15] [INFO ] Loading configuration from database
|
||||||
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
||||||
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||||
|
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 4 (All Debug Messages)
|
||||||
|
```
|
||||||
|
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
||||||
|
[2025-01-12 14:30:15] [DEBUG] Opening database: build/abc123...def.db
|
||||||
|
[2025-01-12 14:30:15] [DEBUG] Executing schema initialization
|
||||||
|
[2025-01-12 14:30:15] [INFO ] SQLite WAL mode enabled
|
||||||
|
[2025-01-12 14:30:16] [DEBUG] Attempting to bind to port 8888
|
||||||
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
||||||
|
[2025-01-12 14:30:17] [DEBUG] Successfully bound to port 8889
|
||||||
|
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 5 (Everything Including file:line for ALL messages)
|
||||||
|
```
|
||||||
|
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay v0.4.6
|
||||||
|
[2025-01-12 14:30:15] [DEBUG] [main.c:348] Opening database: build/abc123...def.db
|
||||||
|
[2025-01-12 14:30:15] [TRACE] [main.c:330] Entering init_database()
|
||||||
|
[2025-01-12 14:30:15] [ERROR] [config.c:125] Database locked
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Create Files (5 minutes)
|
||||||
|
|
||||||
|
1. Create `src/debug.h` with the header code above
|
||||||
|
2. Create `src/debug.c` with the implementation code above
|
||||||
|
3. Update `Makefile` to include `src/debug.c` in `MAIN_SRC`
|
||||||
|
|
||||||
|
### Step 2: Add CLI Parsing (5 minutes)
|
||||||
|
|
||||||
|
Add `--debug-level` argument parsing to `main()` in `src/main.c`
|
||||||
|
|
||||||
|
### Step 3: Update Existing Functions (5 minutes)
|
||||||
|
|
||||||
|
Update the existing `log_*` functions to use the new debug macros
|
||||||
|
|
||||||
|
### Step 4: Test (5 minutes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
make clean && make
|
||||||
|
|
||||||
|
# Test different levels
|
||||||
|
./build/c_relay_x86 # No output
|
||||||
|
./build/c_relay_x86 --debug-level=1 # Errors only
|
||||||
|
./build/c_relay_x86 --debug-level=3 # Info + warnings + errors
|
||||||
|
./build/c_relay_x86 --debug-level=4 # All debug messages
|
||||||
|
./build/c_relay_x86 --debug-level=5 # Everything with file:line on TRACE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Gradual Migration (Ongoing)
|
||||||
|
|
||||||
|
As you work on different parts of the code, replace `log_*` calls with `DEBUG_*` macros and add new debug/trace statements where helpful.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **Simple**: Single flag, 6 levels, easy to understand
|
||||||
|
✅ **Zero Overhead**: Compiler optimizes away unused debug code
|
||||||
|
✅ **Zero Size Impact**: No binary size increase in production
|
||||||
|
✅ **Backward Compatible**: Existing `log_*` functions still work
|
||||||
|
✅ **Easy Migration**: Gradual replacement of log calls
|
||||||
|
✅ **Flexible**: Can add detailed debugging without affecting production
|
||||||
|
|
||||||
|
## Total Implementation Time
|
||||||
|
|
||||||
|
**~20 minutes** for basic implementation
|
||||||
|
**Ongoing** for gradual migration of existing log calls
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
This is the simplest possible debug system that provides:
|
||||||
|
- Multiple debug levels for different verbosity
|
||||||
|
- Zero performance impact in production
|
||||||
|
- Zero binary size increase
|
||||||
|
- Easy to use and understand
|
||||||
|
- Backward compatible with existing code
|
||||||
|
|
||||||
|
Start with the basic implementation, test it, then gradually migrate existing log calls and add new debug statements as needed.
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
# Event-Based Configuration System Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document provides a detailed implementation plan for transitioning the C Nostr Relay from command line arguments and file-based configuration to a pure event-based configuration system using kind 33334 Nostr events stored directly in the database.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 0: File Structure Preparation ✅ COMPLETED
|
|
||||||
|
|
||||||
#### 0.1 Backup and Prepare Files ✅ COMPLETED
|
|
||||||
**Actions:**
|
|
||||||
1. ✅ Rename `src/config.c` to `src/config.c.old` - DONE
|
|
||||||
2. ✅ Rename `src/config.h` to `src/config.h.old` - DONE
|
|
||||||
3. ✅ Create new empty `src/config.c` and `src/config.h` - DONE
|
|
||||||
4. ✅ Create new `src/default_config_event.h` - DONE
|
|
||||||
|
|
||||||
### Phase 1: Database Schema and Core Infrastructure ✅ COMPLETED
|
|
||||||
|
|
||||||
#### 1.1 Update Database Naming System ✅ COMPLETED
|
|
||||||
**File:** `src/main.c`, new `src/config.c`, new `src/config.h`
|
|
||||||
|
|
||||||
```c
|
|
||||||
// New functions implemented: ✅
|
|
||||||
char* get_database_name_from_relay_pubkey(const char* relay_pubkey);
|
|
||||||
int create_database_with_relay_pubkey(const char* relay_pubkey);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes Completed:** ✅
|
|
||||||
- ✅ Create completely new `src/config.c` and `src/config.h` files
|
|
||||||
- ✅ Rename old files to `src/config.c.old` and `src/config.h.old`
|
|
||||||
- ✅ Modify `init_database()` to use relay pubkey for database naming
|
|
||||||
- ✅ Use `nostr_core_lib` functions for all keypair generation
|
|
||||||
- ✅ Database path: `./<relay_pubkey>.nrdb`
|
|
||||||
- ✅ Remove all database path command line argument handling
|
|
||||||
|
|
||||||
#### 1.2 Configuration Event Storage ✅ COMPLETED
|
|
||||||
**File:** new `src/config.c`, new `src/default_config_event.h`
|
|
||||||
|
|
||||||
```c
|
|
||||||
// Configuration functions implemented: ✅
|
|
||||||
int store_config_event_in_database(const cJSON* event);
|
|
||||||
cJSON* load_config_event_from_database(const char* relay_pubkey);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changes Completed:** ✅
|
|
||||||
- ✅ Create new `src/default_config_event.h` for default configuration values
|
|
||||||
- ✅ Add functions to store/retrieve kind 33334 events from events table
|
|
||||||
- ✅ Use `nostr_core_lib` functions for all event validation
|
|
||||||
- ✅ Clean separation: default config values isolated in header file
|
|
||||||
- ✅ Remove existing config table dependencies
|
|
||||||
|
|
||||||
### Phase 2: Event Processing Integration ✅ COMPLETED
|
|
||||||
|
|
||||||
#### 2.1 Real-time Configuration Processing ✅ COMPLETED
|
|
||||||
**File:** `src/main.c` (event processing functions)
|
|
||||||
|
|
||||||
**Integration Points:** ✅ IMPLEMENTED
|
|
||||||
```c
|
|
||||||
// In existing event processing loop: ✅ IMPLEMENTED
|
|
||||||
// Added kind 33334 event detection in main event loop
|
|
||||||
if (kind_num == 33334) {
|
|
||||||
if (handle_configuration_event(event, error_message, sizeof(error_message)) == 0) {
|
|
||||||
// Configuration event processed successfully
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration event processing implemented: ✅
|
|
||||||
int process_configuration_event(const cJSON* event);
|
|
||||||
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 Configuration Application System ⚠️ PARTIALLY COMPLETED
|
|
||||||
**File:** `src/config.c`
|
|
||||||
|
|
||||||
**Status:** Configuration access functions implemented, field handlers need completion
|
|
||||||
```c
|
|
||||||
// Configuration access implemented: ✅
|
|
||||||
const char* get_config_value(const char* key);
|
|
||||||
int get_config_int(const char* key, int default_value);
|
|
||||||
int get_config_bool(const char* key, int default_value);
|
|
||||||
|
|
||||||
// Field handlers need implementation: ⏳ IN PROGRESS
|
|
||||||
// Need to implement specific apply functions for runtime changes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: First-Time Startup System ✅ COMPLETED
|
|
||||||
|
|
||||||
#### 3.1 Key Generation and Initial Setup ✅ COMPLETED
|
|
||||||
**File:** new `src/config.c`, `src/default_config_event.h`
|
|
||||||
|
|
||||||
**Status:** ✅ FULLY IMPLEMENTED with secure /dev/urandom + nostr_core_lib validation
|
|
||||||
|
|
||||||
```c
|
|
||||||
int first_time_startup_sequence() {
|
|
||||||
// 1. Generate admin keypair using nostr_core_lib
|
|
||||||
unsigned char admin_privkey_bytes[32];
|
|
||||||
char admin_privkey[65], admin_pubkey[65];
|
|
||||||
|
|
||||||
if (nostr_generate_private_key(admin_privkey_bytes) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
|
||||||
|
|
||||||
unsigned char admin_pubkey_bytes[32];
|
|
||||||
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
|
|
||||||
|
|
||||||
// 2. Generate relay keypair using nostr_core_lib
|
|
||||||
unsigned char relay_privkey_bytes[32];
|
|
||||||
char relay_privkey[65], relay_pubkey[65];
|
|
||||||
|
|
||||||
if (nostr_generate_private_key(relay_privkey_bytes) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey);
|
|
||||||
|
|
||||||
unsigned char relay_pubkey_bytes[32];
|
|
||||||
if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
nostr_bytes_to_hex(relay_pubkey_bytes, 32, relay_pubkey);
|
|
||||||
|
|
||||||
// 3. Create database with relay pubkey name
|
|
||||||
if (create_database_with_relay_pubkey(relay_pubkey) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Create initial configuration event using defaults from header
|
|
||||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
|
|
||||||
|
|
||||||
// 5. Store configuration event in database
|
|
||||||
store_config_event_in_database(config_event);
|
|
||||||
|
|
||||||
// 6. Print admin private key for user to save
|
|
||||||
printf("=== SAVE THIS ADMIN PRIVATE KEY ===\n");
|
|
||||||
printf("Admin Private Key: %s\n", admin_privkey);
|
|
||||||
printf("===================================\n");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2 Database Detection Logic ✅ COMPLETED
|
|
||||||
**File:** `src/main.c`
|
|
||||||
|
|
||||||
**Status:** ✅ FULLY IMPLEMENTED
|
|
||||||
```c
|
|
||||||
// Implemented functions: ✅
|
|
||||||
char** find_existing_nrdb_files(void);
|
|
||||||
char* extract_pubkey_from_filename(const char* filename);
|
|
||||||
int is_first_time_startup(void);
|
|
||||||
int first_time_startup_sequence(void);
|
|
||||||
int startup_existing_relay(const char* relay_pubkey);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: Legacy System Removal ✅ PARTIALLY COMPLETED
|
|
||||||
|
|
||||||
#### 4.1 Remove Command Line Arguments ✅ COMPLETED
|
|
||||||
**File:** `src/main.c`
|
|
||||||
|
|
||||||
**Status:** ✅ COMPLETED
|
|
||||||
- ✅ All argument parsing logic removed except --help and --version
|
|
||||||
- ✅ `--port`, `--config-dir`, `--config-file`, `--database-path` handling removed
|
|
||||||
- ✅ Environment variable override systems removed
|
|
||||||
- ✅ Clean help and version functions implemented
|
|
||||||
|
|
||||||
#### 4.2 Remove Configuration File System ✅ COMPLETED
|
|
||||||
**File:** `src/config.c`
|
|
||||||
|
|
||||||
**Status:** ✅ COMPLETED - New file created from scratch
|
|
||||||
- ✅ All legacy file-based configuration functions removed
|
|
||||||
- ✅ XDG configuration directory logic removed
|
|
||||||
- ✅ Pure event-based system implemented
|
|
||||||
|
|
||||||
#### 4.3 Remove Legacy Database Tables ⏳ PENDING
|
|
||||||
**File:** `src/sql_schema.h`
|
|
||||||
|
|
||||||
**Status:** ⏳ NEEDS COMPLETION
|
|
||||||
```sql
|
|
||||||
-- Still need to remove these tables:
|
|
||||||
DROP TABLE IF EXISTS config;
|
|
||||||
DROP TABLE IF EXISTS config_history;
|
|
||||||
DROP TABLE IF EXISTS config_file_cache;
|
|
||||||
DROP VIEW IF EXISTS active_config;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 5: Configuration Management
|
|
||||||
|
|
||||||
#### 5.1 Configuration Field Mapping
|
|
||||||
**File:** `src/config.c`
|
|
||||||
|
|
||||||
```c
|
|
||||||
// Map configuration tags to current system
|
|
||||||
static const config_field_handler_t config_handlers[] = {
|
|
||||||
{"auth_enabled", 0, apply_auth_enabled},
|
|
||||||
{"relay_port", 1, apply_relay_port}, // requires restart
|
|
||||||
{"max_connections", 0, apply_max_connections},
|
|
||||||
{"relay_description", 0, apply_relay_description},
|
|
||||||
{"relay_contact", 0, apply_relay_contact},
|
|
||||||
{"relay_pubkey", 1, apply_relay_pubkey}, // requires restart
|
|
||||||
{"relay_privkey", 1, apply_relay_privkey}, // requires restart
|
|
||||||
{"pow_min_difficulty", 0, apply_pow_difficulty},
|
|
||||||
{"nip40_expiration_enabled", 0, apply_expiration_enabled},
|
|
||||||
{"max_subscriptions_per_client", 0, apply_max_subscriptions},
|
|
||||||
{"max_event_tags", 0, apply_max_event_tags},
|
|
||||||
{"max_content_length", 0, apply_max_content_length},
|
|
||||||
{"default_limit", 0, apply_default_limit},
|
|
||||||
{"max_limit", 0, apply_max_limit},
|
|
||||||
// ... etc
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5.2 Startup Configuration Loading
|
|
||||||
**File:** `src/main.c`
|
|
||||||
|
|
||||||
```c
|
|
||||||
int startup_existing_relay(const char* relay_pubkey) {
|
|
||||||
// 1. Open database
|
|
||||||
if (init_database_with_pubkey(relay_pubkey) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Load configuration event from database
|
|
||||||
cJSON* config_event = load_config_event_from_database(relay_pubkey);
|
|
||||||
if (!config_event) {
|
|
||||||
log_error("No configuration event found in database");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Apply all configuration from event
|
|
||||||
if (apply_configuration_from_event(config_event) != 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Continue with normal startup
|
|
||||||
return start_relay_services();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Order - PROGRESS STATUS
|
|
||||||
|
|
||||||
### Step 1: Core Infrastructure ✅ COMPLETED
|
|
||||||
1. ✅ Implement database naming with relay pubkey
|
|
||||||
2. ✅ Add key generation functions using `nostr_core_lib`
|
|
||||||
3. ✅ Create configuration event storage/retrieval functions
|
|
||||||
4. ✅ Test basic event creation and storage
|
|
||||||
|
|
||||||
### Step 2: Event Processing Integration ✅ MOSTLY COMPLETED
|
|
||||||
1. ✅ Add kind 33334 event detection to event processing loop
|
|
||||||
2. ✅ Implement configuration event validation
|
|
||||||
3. ⚠️ Create configuration application handlers (basic access implemented, runtime handlers pending)
|
|
||||||
4. ⏳ Test real-time configuration updates (infrastructure ready)
|
|
||||||
|
|
||||||
### Step 3: First-Time Startup ✅ COMPLETED
|
|
||||||
1. ✅ Implement first-time startup detection
|
|
||||||
2. ✅ Add automatic key generation and database creation
|
|
||||||
3. ✅ Create default configuration event generation
|
|
||||||
4. ✅ Test complete first-time startup flow
|
|
||||||
|
|
||||||
### Step 4: Legacy Removal ⚠️ MOSTLY COMPLETED
|
|
||||||
1. ✅ Remove command line argument parsing
|
|
||||||
2. ✅ Remove configuration file system
|
|
||||||
3. ⏳ Remove legacy database tables (pending)
|
|
||||||
4. ✅ Update all references to use event-based config
|
|
||||||
|
|
||||||
### Step 5: Testing and Validation ⚠️ PARTIALLY COMPLETED
|
|
||||||
1. ✅ Test complete startup flow (first time and existing)
|
|
||||||
2. ⏳ Test configuration updates via events (infrastructure ready)
|
|
||||||
3. ⚠️ Test error handling and recovery (basic error handling implemented)
|
|
||||||
4. ⏳ Performance testing and optimization (pending)
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### For Existing Installations
|
|
||||||
Since the new system uses a completely different approach:
|
|
||||||
|
|
||||||
1. **No Automatic Migration**: The new system starts fresh
|
|
||||||
2. **Manual Migration**: Users can manually copy configuration values
|
|
||||||
3. **Documentation**: Provide clear migration instructions
|
|
||||||
4. **Coexistence**: Old and new systems use different database names
|
|
||||||
|
|
||||||
### Migration Steps for Users
|
|
||||||
1. Stop existing relay
|
|
||||||
2. Note current configuration values
|
|
||||||
3. Start new relay (generates keys and new database)
|
|
||||||
4. Create kind 33334 event with desired configuration using admin private key
|
|
||||||
5. Send event to relay to update configuration
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
- Key generation functions
|
|
||||||
- Configuration event creation and validation
|
|
||||||
- Database naming logic
|
|
||||||
- Configuration application handlers
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- Complete first-time startup flow
|
|
||||||
- Configuration update via events
|
|
||||||
- Error handling scenarios
|
|
||||||
- Database operations
|
|
||||||
|
|
||||||
### Performance Tests
|
|
||||||
- Startup time comparison
|
|
||||||
- Configuration update response time
|
|
||||||
- Memory usage analysis
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Admin Private Key**: Never stored, only printed once
|
|
||||||
2. **Event Validation**: All configuration events must be signed by admin
|
|
||||||
3. **Database Security**: Relay database contains relay private key
|
|
||||||
4. **Key Generation**: Use `nostr_core_lib` for cryptographically secure generation
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
|
|
||||||
### Major Changes
|
|
||||||
- `src/main.c` - Startup logic, event processing, argument removal
|
|
||||||
- `src/config.c` - Complete rewrite for event-based configuration
|
|
||||||
- `src/config.h` - Update function signatures and structures
|
|
||||||
- `src/sql_schema.h` - Remove config tables
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
- `Makefile` - Remove any config file generation
|
|
||||||
- `systemd/` - Update service files if needed
|
|
||||||
- Documentation updates
|
|
||||||
|
|
||||||
## Backwards Compatibility
|
|
||||||
|
|
||||||
**Breaking Changes:**
|
|
||||||
- Command line arguments removed (except --help, --version)
|
|
||||||
- Configuration files no longer used
|
|
||||||
- Database naming scheme changed
|
|
||||||
- Configuration table removed
|
|
||||||
|
|
||||||
**Migration Required:** This is a breaking change that requires manual migration for existing installations.
|
|
||||||
|
|
||||||
## Success Criteria - CURRENT STATUS
|
|
||||||
|
|
||||||
1. ✅ **Zero Command Line Arguments**: Relay starts with just `./c-relay`
|
|
||||||
2. ✅ **Automatic First-Time Setup**: Generates keys and database automatically
|
|
||||||
3. ⚠️ **Real-Time Configuration**: Infrastructure ready, handlers need completion
|
|
||||||
4. ✅ **Single Database File**: All configuration and data in one `.nrdb` file
|
|
||||||
5. ⚠️ **Admin Control**: Event processing implemented, signature validation ready
|
|
||||||
6. ⚠️ **Clean Codebase**: Most legacy code removed, database tables cleanup pending
|
|
||||||
|
|
||||||
## Risk Mitigation
|
|
||||||
|
|
||||||
1. **Backup Strategy**: Document manual backup procedures for relay database
|
|
||||||
2. **Key Loss Recovery**: Document recovery procedures if admin key is lost
|
|
||||||
3. **Testing Coverage**: Comprehensive test suite before deployment
|
|
||||||
4. **Rollback Plan**: Keep old version available during transition period
|
|
||||||
5. **Documentation**: Comprehensive user and developer documentation
|
|
||||||
|
|
||||||
This implementation plan provides a clear path from the current system to the new event-based configuration architecture while maintaining security and reliability.
|
|
||||||
275
docs/musl_static_build.md
Normal file
275
docs/musl_static_build.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# MUSL Static Binary Build Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide explains how to build truly portable MUSL-based static binaries of c-relay using Alpine Linux Docker containers. These binaries have **zero runtime dependencies** and work on any Linux distribution.
|
||||||
|
|
||||||
|
## Why MUSL?
|
||||||
|
|
||||||
|
### MUSL vs glibc Static Binaries
|
||||||
|
|
||||||
|
**MUSL Advantages:**
|
||||||
|
- **Truly Static**: No hidden dependencies on system libraries
|
||||||
|
- **Smaller Size**: ~7.6MB vs ~12MB+ for glibc static builds
|
||||||
|
- **Better Portability**: Works on ANY Linux distribution without modification
|
||||||
|
- **Cleaner Linking**: No glibc-specific extensions or fortified functions
|
||||||
|
- **Simpler Deployment**: Single binary, no library compatibility issues
|
||||||
|
|
||||||
|
**glibc Limitations:**
|
||||||
|
- Static builds still require dynamic loading for NSS (Name Service Switch)
|
||||||
|
- Fortified functions (`__*_chk`) don't exist in MUSL
|
||||||
|
- Larger binary size due to glibc's complexity
|
||||||
|
- May have compatibility issues across different glibc versions
|
||||||
|
|
||||||
|
## Build Process
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running
|
||||||
|
- Sufficient disk space (~2GB for Docker layers)
|
||||||
|
- Internet connection (for downloading dependencies)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build MUSL static binary
|
||||||
|
./build_static.sh
|
||||||
|
|
||||||
|
# The binary will be created at:
|
||||||
|
# build/c_relay_static_musl_x86_64 (on x86_64)
|
||||||
|
# build/c_relay_static_musl_arm64 (on ARM64)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Happens During Build
|
||||||
|
|
||||||
|
1. **Alpine Linux Base**: Uses Alpine 3.19 with native MUSL support
|
||||||
|
2. **Static Dependencies**: Builds all dependencies with static linking:
|
||||||
|
- libsecp256k1 (Bitcoin cryptography)
|
||||||
|
- libwebsockets (WebSocket server)
|
||||||
|
- OpenSSL (TLS/crypto)
|
||||||
|
- SQLite (database)
|
||||||
|
- curl (HTTP client)
|
||||||
|
- zlib (compression)
|
||||||
|
|
||||||
|
3. **nostr_core_lib**: Builds with MUSL-compatible flags:
|
||||||
|
- Disables glibc fortification (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`)
|
||||||
|
- Includes required NIPs: 001, 006, 013, 017, 019, 044, 059
|
||||||
|
- Produces static library (~316KB)
|
||||||
|
|
||||||
|
4. **c-relay Compilation**: Links everything statically:
|
||||||
|
- All source files compiled with `-static` flag
|
||||||
|
- Fortification disabled to avoid `__*_chk` symbols
|
||||||
|
- Results in ~7.6MB stripped binary
|
||||||
|
|
||||||
|
5. **Verification**: Confirms binary is truly static:
|
||||||
|
- `ldd` shows "not a dynamic executable"
|
||||||
|
- `file` shows "statically linked"
|
||||||
|
- Binary executes successfully
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Dockerfile Structure
|
||||||
|
|
||||||
|
The build uses a multi-stage Dockerfile (`Dockerfile.alpine-musl`):
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Builder (Alpine Linux)
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
- Install build tools and static libraries
|
||||||
|
- Build dependencies from source
|
||||||
|
- Compile nostr_core_lib with MUSL flags
|
||||||
|
- Compile c-relay with full static linking
|
||||||
|
- Strip binary to reduce size
|
||||||
|
|
||||||
|
# Stage 2: Output (scratch)
|
||||||
|
FROM scratch AS output
|
||||||
|
- Contains only the final binary
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Compilation Flags
|
||||||
|
|
||||||
|
**For nostr_core_lib:**
|
||||||
|
```bash
|
||||||
|
CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"
|
||||||
|
```
|
||||||
|
|
||||||
|
**For c-relay:**
|
||||||
|
```bash
|
||||||
|
gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
[source files] \
|
||||||
|
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||||
|
-lcurl -lz -lpthread -lm -ldl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fortification Issue
|
||||||
|
|
||||||
|
**Problem**: GCC's `-O2` optimization enables fortification by default, replacing standard functions with `__*_chk` variants (e.g., `__snprintf_chk`, `__fprintf_chk`). These are glibc-specific and don't exist in MUSL.
|
||||||
|
|
||||||
|
**Solution**: Explicitly disable fortification with:
|
||||||
|
- `-U_FORTIFY_SOURCE` (undefine any existing definition)
|
||||||
|
- `-D_FORTIFY_SOURCE=0` (set to 0)
|
||||||
|
|
||||||
|
This must be applied to **both** nostr_core_lib and c-relay compilation.
|
||||||
|
|
||||||
|
### NIP Dependencies
|
||||||
|
|
||||||
|
The build includes these NIPs in nostr_core_lib:
|
||||||
|
- **NIP-001**: Basic protocol (event creation, signing)
|
||||||
|
- **NIP-006**: Key derivation from mnemonic
|
||||||
|
- **NIP-013**: Proof of Work validation
|
||||||
|
- **NIP-017**: Private Direct Messages
|
||||||
|
- **NIP-019**: Bech32 encoding (nsec/npub)
|
||||||
|
- **NIP-044**: Modern encryption
|
||||||
|
- **NIP-059**: Gift Wrap (required by NIP-017)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Check Binary Type
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Should show "statically linked"
|
||||||
|
file build/c_relay_static_musl_x86_64
|
||||||
|
|
||||||
|
# Should show "not a dynamic executable"
|
||||||
|
ldd build/c_relay_static_musl_x86_64
|
||||||
|
|
||||||
|
# Check size (should be ~7.6MB)
|
||||||
|
ls -lh build/c_relay_static_musl_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help
|
||||||
|
./build/c_relay_static_musl_x86_64 --help
|
||||||
|
|
||||||
|
# Show version
|
||||||
|
./build/c_relay_static_musl_x86_64 --version
|
||||||
|
|
||||||
|
# Run relay
|
||||||
|
./build/c_relay_static_musl_x86_64 --port 8888
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Distribution Testing
|
||||||
|
|
||||||
|
Test the binary on different distributions to verify portability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alpine Linux
|
||||||
|
docker run --rm -v $(pwd)/build:/app alpine:latest /app/c_relay_static_musl_x86_64 --version
|
||||||
|
|
||||||
|
# Ubuntu
|
||||||
|
docker run --rm -v $(pwd)/build:/app ubuntu:latest /app/c_relay_static_musl_x86_64 --version
|
||||||
|
|
||||||
|
# Debian
|
||||||
|
docker run --rm -v $(pwd)/build:/app debian:latest /app/c_relay_static_musl_x86_64 --version
|
||||||
|
|
||||||
|
# CentOS
|
||||||
|
docker run --rm -v $(pwd)/build:/app centos:latest /app/c_relay_static_musl_x86_64 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Docker Permission Denied
|
||||||
|
|
||||||
|
**Problem**: `permission denied while trying to connect to the Docker daemon socket`
|
||||||
|
|
||||||
|
**Solution**: Add user to docker group:
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
newgrp docker # Or logout and login again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Fails with Fortification Errors
|
||||||
|
|
||||||
|
**Problem**: `undefined reference to '__snprintf_chk'` or `'__fprintf_chk'`
|
||||||
|
|
||||||
|
**Solution**: Ensure fortification is disabled in both:
|
||||||
|
1. nostr_core_lib build.sh (line 534)
|
||||||
|
2. c-relay compilation flags in Dockerfile
|
||||||
|
|
||||||
|
### Binary Won't Execute
|
||||||
|
|
||||||
|
**Problem**: Binary fails to run on target system
|
||||||
|
|
||||||
|
**Checks**:
|
||||||
|
1. Verify it's truly static: `ldd binary` should show "not a dynamic executable"
|
||||||
|
2. Check architecture matches: `file binary` should show correct arch
|
||||||
|
3. Ensure execute permissions: `chmod +x binary`
|
||||||
|
|
||||||
|
### Missing NIP Functions
|
||||||
|
|
||||||
|
**Problem**: `undefined reference to 'nostr_nip*'` during linking
|
||||||
|
|
||||||
|
**Solution**: Add missing NIPs to the build command:
|
||||||
|
```bash
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Single Binary Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy binary to server
|
||||||
|
scp build/c_relay_static_musl_x86_64 user@server:/opt/c-relay/
|
||||||
|
|
||||||
|
# Run on server (no dependencies needed!)
|
||||||
|
ssh user@server
|
||||||
|
cd /opt/c-relay
|
||||||
|
./c_relay_static_musl_x86_64 --port 8888
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemD Service
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=C-Relay Nostr Relay (MUSL Static)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=c-relay
|
||||||
|
WorkingDirectory=/opt/c-relay
|
||||||
|
ExecStart=/opt/c-relay/c_relay_static_musl_x86_64
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Comparison
|
||||||
|
|
||||||
|
| Metric | MUSL Static | glibc Static | glibc Dynamic |
|
||||||
|
|--------|-------------|--------------|---------------|
|
||||||
|
| Binary Size | 7.6 MB | 12+ MB | 2-3 MB |
|
||||||
|
| Startup Time | ~50ms | ~60ms | ~40ms |
|
||||||
|
| Memory Usage | Similar | Similar | Similar |
|
||||||
|
| Portability | ✓ Any Linux | ⚠ glibc only | ✗ Requires libs |
|
||||||
|
| Dependencies | None | NSS libs | Many libs |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always verify** the binary is truly static before deployment
|
||||||
|
2. **Test on multiple distributions** to ensure portability
|
||||||
|
3. **Keep Docker images updated** for security patches
|
||||||
|
4. **Document the build date** and commit hash for reproducibility
|
||||||
|
5. **Store binaries** with architecture in filename (e.g., `_x86_64`, `_arm64`)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MUSL libc](https://musl.libc.org/)
|
||||||
|
- [Alpine Linux](https://alpinelinux.org/)
|
||||||
|
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||||
|
- [GCC Fortification](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 2025-10-11
|
||||||
|
- Initial MUSL build system implementation
|
||||||
|
- Alpine Docker-based build process
|
||||||
|
- Fortification fix for nostr_core_lib
|
||||||
|
- Complete NIP dependency resolution
|
||||||
|
- Documentation created
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Startup Configuration Design Analysis
|
|
||||||
|
|
||||||
## Review of startup_config_design.md
|
|
||||||
|
|
||||||
### Key Design Principles Identified
|
|
||||||
|
|
||||||
1. **Zero Command Line Arguments**: Complete elimination of CLI arguments for true "quick start"
|
|
||||||
2. **Event-Based Configuration**: Configuration stored as Nostr event (kind 33334) in events table
|
|
||||||
3. **Self-Contained Database**: Database named after relay pubkey (`<pubkey>.nrdb`)
|
|
||||||
4. **First-Time Setup**: Automatic key generation and initial configuration creation
|
|
||||||
5. **Configuration Consistency**: Always read from event, never from hardcoded defaults
|
|
||||||
|
|
||||||
### Implementation Gaps and Specifications Needed
|
|
||||||
|
|
||||||
#### 1. Key Generation Process
|
|
||||||
**Specification:**
|
|
||||||
```
|
|
||||||
First Startup Key Generation:
|
|
||||||
1. Generate all keys on first startup (admin private/public, relay private/public)
|
|
||||||
2. Use nostr_core_lib for key generation entropy
|
|
||||||
3. Keys are encoded in hex format
|
|
||||||
4. Print admin private key to stdout for user to save (never stored)
|
|
||||||
5. Store admin public key, relay private key, and relay public key in configuration event
|
|
||||||
6. Admin can later change the 33334 event to alter stored keys
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Database Naming and Location
|
|
||||||
**Specification:**
|
|
||||||
```
|
|
||||||
Database Naming:
|
|
||||||
1. Database is named using relay pubkey: ./<relay_pubkey>.nrdb
|
|
||||||
2. Database path structure: ./<relay_pubkey>.nrdb
|
|
||||||
3. If database creation fails, program quits (can't run without database)
|
|
||||||
4. c_nostr_relay.db should never exist in new system
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Configuration Event Structure (Kind 33334)
|
|
||||||
**Specification:**
|
|
||||||
```
|
|
||||||
Event Structure:
|
|
||||||
- Kind: 33334 (parameterized replaceable event)
|
|
||||||
- Event validation: Use nostr_core_lib to validate event
|
|
||||||
- Event content field: "C Nostr Relay Configuration" (descriptive text)
|
|
||||||
- Configuration update mechanism: TBD
|
|
||||||
- Complete tag structure provided in configuration section below
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 4. Configuration Change Monitoring
|
|
||||||
**Configuration Monitoring System:**
|
|
||||||
```
|
|
||||||
Every event that is received is checked to see if it is a kind 33334 event from the admin pubkey.
|
|
||||||
If so, it is processed as a configuration update.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Error Handling and Recovery
|
|
||||||
**Specification:**
|
|
||||||
```
|
|
||||||
Error Recovery Priority:
|
|
||||||
1. Try to load latest valid config event
|
|
||||||
2. Generate new default configuration event if none exists
|
|
||||||
3. Exit with error if all recovery attempts fail
|
|
||||||
|
|
||||||
Note: There is only ever one configuration event (parameterized replaceable event),
|
|
||||||
so no fallback to previous versions.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Clarifications
|
|
||||||
|
|
||||||
**Key Management:**
|
|
||||||
- Admin private key is never stored, only printed once at first startup
|
|
||||||
- Single admin system (no multi-admin support)
|
|
||||||
- No key rotation support
|
|
||||||
|
|
||||||
**Configuration Management:**
|
|
||||||
- No configuration versioning/timestamping
|
|
||||||
- No automatic backup of configuration events
|
|
||||||
- Configuration events are not broadcastable to other relays
|
|
||||||
- Future: Auth system to restrict admin access to configuration events
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Complete Current Configuration Structure
|
|
||||||
|
|
||||||
Based on analysis of [`src/config.c`](src/config.c:753-795), here is the complete current configuration structure that will be converted to event tags:
|
|
||||||
|
|
||||||
### Complete Event Structure Example
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kind": 33334,
|
|
||||||
"created_at": 1725661483,
|
|
||||||
"tags": [
|
|
||||||
["d", "<relay_pubkey>"],
|
|
||||||
["auth_enabled", "false"],
|
|
||||||
["relay_port", "8888"],
|
|
||||||
["max_connections", "100"],
|
|
||||||
|
|
||||||
["relay_description", "High-performance C Nostr relay with SQLite storage"],
|
|
||||||
["relay_contact", ""],
|
|
||||||
["relay_pubkey", "<relay_public_key>"],
|
|
||||||
["relay_privkey", "<relay_private_key>"],
|
|
||||||
["relay_software", "https://git.laantungir.net/laantungir/c-relay.git"],
|
|
||||||
["relay_version", "v1.0.0"],
|
|
||||||
|
|
||||||
["pow_min_difficulty", "0"],
|
|
||||||
["pow_mode", "basic"],
|
|
||||||
["nip40_expiration_enabled", "true"],
|
|
||||||
["nip40_expiration_strict", "true"],
|
|
||||||
["nip40_expiration_filter", "true"],
|
|
||||||
["nip40_expiration_grace_period", "300"],
|
|
||||||
["max_subscriptions_per_client", "25"],
|
|
||||||
["max_total_subscriptions", "5000"],
|
|
||||||
["max_filters_per_subscription", "10"],
|
|
||||||
["max_event_tags", "100"],
|
|
||||||
["max_content_length", "8196"],
|
|
||||||
["max_message_length", "16384"],
|
|
||||||
["default_limit", "500"],
|
|
||||||
["max_limit", "5000"]
|
|
||||||
],
|
|
||||||
"content": "C Nostr Relay Configuration",
|
|
||||||
"pubkey": "<admin_public_key>",
|
|
||||||
"id": "<computed_event_id>",
|
|
||||||
"sig": "<event_signature>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** The `admin_pubkey` tag is omitted as it's redundant with the event's `pubkey` field.
|
|
||||||
1090
docs/startup_flows_complete.md
Normal file
1090
docs/startup_flows_complete.md
Normal file
File diff suppressed because it is too large
Load Diff
147
docs/static_build_improvements.md
Normal file
147
docs/static_build_improvements.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Static Build Improvements
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `build_static.sh` script has been updated to properly support MUSL static compilation and includes several optimizations.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. True MUSL Static Binary Support
|
||||||
|
|
||||||
|
The script now attempts to build with `musl-gcc` for truly portable static binaries:
|
||||||
|
|
||||||
|
- **MUSL binaries** have zero runtime dependencies and work across all Linux distributions
|
||||||
|
- **Automatic fallback** to glibc static linking if MUSL compilation fails (e.g., missing MUSL-compiled libraries)
|
||||||
|
- Clear messaging about which type of binary was created
|
||||||
|
|
||||||
|
### 2. SQLite Build Caching
|
||||||
|
|
||||||
|
SQLite is now built once and cached for future builds:
|
||||||
|
|
||||||
|
- **Cache location**: `~/.cache/c-relay-sqlite/`
|
||||||
|
- **Version-specific**: Each SQLite version gets its own cache directory
|
||||||
|
- **Significant speedup**: Subsequent builds skip the SQLite compilation step
|
||||||
|
- **Manual cleanup**: `rm -rf ~/.cache/c-relay-sqlite` to clear cache
|
||||||
|
|
||||||
|
### 3. Smart Package Installation
|
||||||
|
|
||||||
|
The script now checks for required packages before installing:
|
||||||
|
|
||||||
|
- Only installs missing packages
|
||||||
|
- Reduces unnecessary `apt` operations
|
||||||
|
- Faster builds when dependencies are already present
|
||||||
|
|
||||||
|
### 4. Bug Fixes
|
||||||
|
|
||||||
|
- Fixed format warning in `src/subscriptions.c` line 1067 (changed `%zu` to `%d` with cast for `MAX_SEARCH_TERM_LENGTH`)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_static.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
1. Check for and install `musl-gcc` if needed
|
||||||
|
2. Build or use cached SQLite with JSON1 support
|
||||||
|
3. Attempt MUSL static compilation
|
||||||
|
4. Fall back to glibc static compilation if MUSL fails
|
||||||
|
5. Verify the resulting binary
|
||||||
|
|
||||||
|
## Binary Types
|
||||||
|
|
||||||
|
### MUSL Static Binary (Ideal - Currently Not Achievable)
|
||||||
|
- **Filename**: `build/c_relay_static_musl_x86_64`
|
||||||
|
- **Dependencies**: None (truly static)
|
||||||
|
- **Portability**: Works on any Linux distribution
|
||||||
|
- **Status**: Requires MUSL-compiled libwebsockets and other dependencies (not available by default)
|
||||||
|
|
||||||
|
### Glibc Static Binary (Current Output)
|
||||||
|
- **Filename**: `build/c_relay_static_x86_64` or `build/c_relay_static_glibc_x86_64`
|
||||||
|
- **Dependencies**: None - fully statically linked with glibc
|
||||||
|
- **Portability**: Works on most Linux distributions (glibc is statically included)
|
||||||
|
- **Note**: Despite using glibc, this is a **fully static binary** with no runtime dependencies
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
The script automatically verifies binaries using `ldd` and `file`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For MUSL binary
|
||||||
|
ldd build/c_relay_static_musl_x86_64
|
||||||
|
# Output: "not a dynamic executable" (good!)
|
||||||
|
|
||||||
|
# For glibc binary
|
||||||
|
ldd build/c_relay_static_glibc_x86_64
|
||||||
|
# Output: Shows glibc dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### MUSL Compilation Currently Fails Because:
|
||||||
|
|
||||||
|
1. **libwebsockets not available as MUSL static library**
|
||||||
|
- System libwebsockets is compiled with glibc, not MUSL
|
||||||
|
- MUSL cannot link against glibc-compiled libraries
|
||||||
|
- Solution: Build libwebsockets from source with musl-gcc (future enhancement)
|
||||||
|
|
||||||
|
2. **Other dependencies not MUSL-compatible**
|
||||||
|
- libssl, libcrypto, libsecp256k1, libcurl must be available as MUSL static libraries
|
||||||
|
- Most systems only provide glibc versions
|
||||||
|
- Solution: Build entire dependency chain with musl-gcc (complex, future enhancement)
|
||||||
|
|
||||||
|
### Current Behavior
|
||||||
|
|
||||||
|
The script attempts MUSL compilation but falls back to glibc:
|
||||||
|
1. Tries to compile with `musl-gcc -static` (fails due to missing MUSL libraries)
|
||||||
|
2. Logs the error to `/tmp/musl_build.log`
|
||||||
|
3. Displays a clear warning message
|
||||||
|
4. Automatically falls back to `gcc -static` with glibc
|
||||||
|
5. Produces a **fully static binary** with glibc statically linked (no runtime dependencies)
|
||||||
|
|
||||||
|
**Important**: The glibc static binary is still fully portable across most Linux distributions because glibc is statically included in the binary. It's not as universally portable as MUSL would be, but it works on virtually all modern Linux systems.
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Full MUSL dependency chain**: Build all dependencies (libwebsockets, OpenSSL, etc.) with musl-gcc
|
||||||
|
2. **Multi-architecture support**: Add ARM64 MUSL builds
|
||||||
|
3. **Docker-based builds**: Use Alpine Linux containers for guaranteed MUSL environment
|
||||||
|
4. **Dependency vendoring**: Include pre-built MUSL libraries in the repository
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Clear SQLite Cache
|
||||||
|
```bash
|
||||||
|
rm -rf ~/.cache/c-relay-sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Force Package Reinstall
|
||||||
|
```bash
|
||||||
|
sudo apt install --reinstall musl-dev musl-tools libssl-dev libcurl4-openssl-dev libsecp256k1-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Build Logs
|
||||||
|
```bash
|
||||||
|
cat /tmp/musl_build.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Binary Type
|
||||||
|
```bash
|
||||||
|
file build/c_relay_static_*
|
||||||
|
ldd build/c_relay_static_* 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
- **First build**: ~2-3 minutes (includes SQLite compilation)
|
||||||
|
- **Subsequent builds**: ~30-60 seconds (uses cached SQLite)
|
||||||
|
- **Cache size**: ~10-15 MB per SQLite version
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The updated script is compatible with:
|
||||||
|
- Ubuntu 20.04+
|
||||||
|
- Debian 10+
|
||||||
|
- Other Debian-based distributions with `apt` package manager
|
||||||
|
|
||||||
|
For other distributions, adjust package installation commands accordingly.
|
||||||
427
docs/unified_startup_design.md
Normal file
427
docs/unified_startup_design.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
# Unified Startup Sequence Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the new unified startup sequence where all config values are created first, then CLI overrides are applied as a separate atomic operation. This eliminates the current 3-step incremental building process.
|
||||||
|
|
||||||
|
## Current Problems
|
||||||
|
|
||||||
|
1. **Incremental Config Building**: Config is built in 3 steps:
|
||||||
|
- Step 1: `populate_default_config_values()` - adds defaults
|
||||||
|
- Step 2: CLI overrides applied via `update_config_in_table()`
|
||||||
|
- Step 3: `add_pubkeys_to_config_table()` - adds generated keys
|
||||||
|
|
||||||
|
2. **Race Conditions**: Cache can be refreshed between steps, causing incomplete config reads
|
||||||
|
|
||||||
|
3. **Complexity**: Multiple code paths for first-time vs restart scenarios
|
||||||
|
|
||||||
|
## New Design Principles
|
||||||
|
|
||||||
|
1. **Atomic Config Creation**: All config values created in single transaction
|
||||||
|
2. **Separate Override Phase**: CLI overrides applied after complete config exists
|
||||||
|
3. **Unified Code Path**: Same logic for first-time and restart scenarios
|
||||||
|
4. **Cache Safety**: Cache only loaded after config is complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 1: First-Time Startup (No Database)
|
||||||
|
|
||||||
|
### Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Key Generation Phase
|
||||||
|
├─ generate_random_private_key_bytes() → admin_privkey_bytes
|
||||||
|
├─ nostr_bytes_to_hex() → admin_privkey (hex)
|
||||||
|
├─ nostr_ec_public_key_from_private_key() → admin_pubkey_bytes
|
||||||
|
├─ nostr_bytes_to_hex() → admin_pubkey (hex)
|
||||||
|
├─ generate_random_private_key_bytes() → relay_privkey_bytes
|
||||||
|
├─ nostr_bytes_to_hex() → relay_privkey (hex)
|
||||||
|
├─ nostr_ec_public_key_from_private_key() → relay_pubkey_bytes
|
||||||
|
└─ nostr_bytes_to_hex() → relay_pubkey (hex)
|
||||||
|
|
||||||
|
2. Database Creation Phase
|
||||||
|
├─ create_database_with_relay_pubkey(relay_pubkey)
|
||||||
|
│ └─ Sets g_database_path = "<relay_pubkey>.db"
|
||||||
|
└─ init_database(g_database_path)
|
||||||
|
└─ Creates database with embedded schema (includes config table)
|
||||||
|
|
||||||
|
3. Complete Config Population Phase (ATOMIC)
|
||||||
|
├─ BEGIN TRANSACTION
|
||||||
|
├─ populate_all_config_values_atomic()
|
||||||
|
│ ├─ Insert ALL default config values from DEFAULT_CONFIG_VALUES[]
|
||||||
|
│ ├─ Insert admin_pubkey
|
||||||
|
│ └─ Insert relay_pubkey
|
||||||
|
└─ COMMIT TRANSACTION
|
||||||
|
|
||||||
|
4. CLI Override Phase (ATOMIC)
|
||||||
|
├─ BEGIN TRANSACTION
|
||||||
|
├─ apply_cli_overrides()
|
||||||
|
│ ├─ IF cli_options.port_override > 0:
|
||||||
|
│ │ └─ UPDATE config SET value = ? WHERE key = 'relay_port'
|
||||||
|
│ ├─ IF cli_options.admin_pubkey_override[0]:
|
||||||
|
│ │ └─ UPDATE config SET value = ? WHERE key = 'admin_pubkey'
|
||||||
|
│ └─ IF cli_options.relay_privkey_override[0]:
|
||||||
|
│ └─ UPDATE config SET value = ? WHERE key = 'relay_privkey'
|
||||||
|
└─ COMMIT TRANSACTION
|
||||||
|
|
||||||
|
5. Secure Key Storage Phase
|
||||||
|
└─ store_relay_private_key(relay_privkey)
|
||||||
|
└─ INSERT INTO relay_seckey (private_key_hex) VALUES (?)
|
||||||
|
|
||||||
|
6. Cache Initialization Phase
|
||||||
|
└─ refresh_unified_cache_from_table()
|
||||||
|
└─ Loads complete config into g_unified_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Call Sequence
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In main.c - first_time_startup branch
|
||||||
|
if (is_first_time_startup()) {
|
||||||
|
// 1. Key Generation
|
||||||
|
first_time_startup_sequence(&cli_options);
|
||||||
|
// → Generates keys, stores in g_unified_cache
|
||||||
|
// → Sets g_database_path
|
||||||
|
// → Does NOT populate config yet
|
||||||
|
|
||||||
|
// 2. Database Creation
|
||||||
|
init_database(g_database_path);
|
||||||
|
// → Creates database with schema
|
||||||
|
|
||||||
|
// 3. Complete Config Population (NEW FUNCTION)
|
||||||
|
populate_all_config_values_atomic(&cli_options);
|
||||||
|
// → Inserts ALL defaults + pubkeys in single transaction
|
||||||
|
// → Does NOT apply CLI overrides yet
|
||||||
|
|
||||||
|
// 4. CLI Override Phase (NEW FUNCTION)
|
||||||
|
apply_cli_overrides_atomic(&cli_options);
|
||||||
|
// → Updates config table with CLI overrides
|
||||||
|
// → Separate transaction after complete config exists
|
||||||
|
|
||||||
|
// 5. Secure Key Storage
|
||||||
|
store_relay_private_key(relay_privkey);
|
||||||
|
|
||||||
|
// 6. Cache Initialization
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Functions Needed
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In config.c
|
||||||
|
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
|
||||||
|
// BEGIN TRANSACTION
|
||||||
|
// Insert ALL defaults from DEFAULT_CONFIG_VALUES[]
|
||||||
|
// Insert admin_pubkey from g_unified_cache
|
||||||
|
// Insert relay_pubkey from g_unified_cache
|
||||||
|
// COMMIT TRANSACTION
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
|
||||||
|
// BEGIN TRANSACTION
|
||||||
|
// IF port_override: UPDATE config SET value = ? WHERE key = 'relay_port'
|
||||||
|
// IF admin_pubkey_override: UPDATE config SET value = ? WHERE key = 'admin_pubkey'
|
||||||
|
// IF relay_privkey_override: UPDATE config SET value = ? WHERE key = 'relay_privkey'
|
||||||
|
// COMMIT TRANSACTION
|
||||||
|
// invalidate_config_cache()
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 2: Restart with Existing Database + CLI Options
|
||||||
|
|
||||||
|
### Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Database Discovery Phase
|
||||||
|
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
|
||||||
|
├─ extract_pubkey_from_filename() → relay_pubkey
|
||||||
|
└─ Sets g_database_path = "<relay_pubkey>.db"
|
||||||
|
|
||||||
|
2. Database Initialization Phase
|
||||||
|
└─ init_database(g_database_path)
|
||||||
|
└─ Opens existing database
|
||||||
|
|
||||||
|
3. Config Validation Phase
|
||||||
|
└─ validate_config_table_completeness()
|
||||||
|
├─ Check if all required keys exist
|
||||||
|
└─ IF missing keys: populate_missing_config_values()
|
||||||
|
|
||||||
|
4. CLI Override Phase (ATOMIC)
|
||||||
|
├─ BEGIN TRANSACTION
|
||||||
|
├─ apply_cli_overrides()
|
||||||
|
│ └─ UPDATE config SET value = ? WHERE key = ?
|
||||||
|
└─ COMMIT TRANSACTION
|
||||||
|
|
||||||
|
5. Cache Initialization Phase
|
||||||
|
└─ refresh_unified_cache_from_table()
|
||||||
|
└─ Loads complete config into g_unified_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Call Sequence
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In main.c - existing relay branch
|
||||||
|
else {
|
||||||
|
// 1. Database Discovery
|
||||||
|
char** existing_files = find_existing_db_files();
|
||||||
|
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||||
|
startup_existing_relay(relay_pubkey);
|
||||||
|
// → Sets g_database_path
|
||||||
|
|
||||||
|
// 2. Database Initialization
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// 3. Config Validation (NEW FUNCTION)
|
||||||
|
validate_config_table_completeness();
|
||||||
|
// → Checks for missing keys
|
||||||
|
// → Populates any missing defaults
|
||||||
|
|
||||||
|
// 4. CLI Override Phase (REUSE FUNCTION)
|
||||||
|
if (has_cli_overrides(&cli_options)) {
|
||||||
|
apply_cli_overrides_atomic(&cli_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Cache Initialization
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Functions Needed
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In config.c
|
||||||
|
int validate_config_table_completeness(void) {
|
||||||
|
// Check if all DEFAULT_CONFIG_VALUES keys exist
|
||||||
|
// IF missing: populate_missing_config_values()
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int populate_missing_config_values(void) {
|
||||||
|
// BEGIN TRANSACTION
|
||||||
|
// For each key in DEFAULT_CONFIG_VALUES:
|
||||||
|
// IF NOT EXISTS: INSERT INTO config
|
||||||
|
// COMMIT TRANSACTION
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int has_cli_overrides(const cli_options_t* cli_options) {
|
||||||
|
return (cli_options->port_override > 0 ||
|
||||||
|
cli_options->admin_pubkey_override[0] != '\0' ||
|
||||||
|
cli_options->relay_privkey_override[0] != '\0');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 3: Restart with Existing Database + No CLI Options
|
||||||
|
|
||||||
|
### Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Database Discovery Phase
|
||||||
|
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
|
||||||
|
├─ extract_pubkey_from_filename() → relay_pubkey
|
||||||
|
└─ Sets g_database_path = "<relay_pubkey>.db"
|
||||||
|
|
||||||
|
2. Database Initialization Phase
|
||||||
|
└─ init_database(g_database_path)
|
||||||
|
└─ Opens existing database
|
||||||
|
|
||||||
|
3. Config Validation Phase
|
||||||
|
└─ validate_config_table_completeness()
|
||||||
|
├─ Check if all required keys exist
|
||||||
|
└─ IF missing keys: populate_missing_config_values()
|
||||||
|
|
||||||
|
4. Cache Initialization Phase (IMMEDIATE)
|
||||||
|
└─ refresh_unified_cache_from_table()
|
||||||
|
└─ Loads complete config into g_unified_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Call Sequence
|
||||||
|
|
||||||
|
```c
|
||||||
|
// In main.c - existing relay branch (no CLI overrides)
|
||||||
|
else {
|
||||||
|
// 1. Database Discovery
|
||||||
|
char** existing_files = find_existing_db_files();
|
||||||
|
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||||
|
startup_existing_relay(relay_pubkey);
|
||||||
|
|
||||||
|
// 2. Database Initialization
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// 3. Config Validation
|
||||||
|
validate_config_table_completeness();
|
||||||
|
|
||||||
|
// 4. Cache Initialization (IMMEDIATE - no overrides to apply)
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Improvements
|
||||||
|
|
||||||
|
### 1. Atomic Config Creation
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```c
|
||||||
|
populate_default_config_values(); // Step 1
|
||||||
|
update_config_in_table("relay_port", port_str); // Step 2
|
||||||
|
add_pubkeys_to_config_table(); // Step 3
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```c
|
||||||
|
populate_all_config_values_atomic(&cli_options); // Single transaction
|
||||||
|
apply_cli_overrides_atomic(&cli_options); // Separate transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Elimination of Race Conditions
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- Cache could refresh between steps 1-3
|
||||||
|
- Incomplete config could be read
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Config created atomically
|
||||||
|
- Cache only refreshed after complete config exists
|
||||||
|
|
||||||
|
### 3. Unified Code Path
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- Different logic for first-time vs restart
|
||||||
|
- `populate_default_config_values()` vs `add_pubkeys_to_config_table()`
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Same validation logic for both scenarios
|
||||||
|
- `validate_config_table_completeness()` handles both cases
|
||||||
|
|
||||||
|
### 4. Clear Separation of Concerns
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- CLI overrides mixed with default population
|
||||||
|
- Unclear when overrides are applied
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Phase 1: Complete config creation
|
||||||
|
- Phase 2: CLI overrides (if any)
|
||||||
|
- Phase 3: Cache initialization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Changes Required
|
||||||
|
|
||||||
|
### 1. New Functions in config.c
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Atomic config population for first-time startup
|
||||||
|
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||||
|
|
||||||
|
// Atomic CLI override application
|
||||||
|
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||||
|
|
||||||
|
// Config validation for existing databases
|
||||||
|
int validate_config_table_completeness(void);
|
||||||
|
int populate_missing_config_values(void);
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
int has_cli_overrides(const cli_options_t* cli_options);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Modified Functions in config.c
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Simplify to only generate keys and set database path
|
||||||
|
int first_time_startup_sequence(const cli_options_t* cli_options);
|
||||||
|
|
||||||
|
// Remove config population logic
|
||||||
|
int add_pubkeys_to_config_table(void); // DEPRECATED - logic moved to populate_all_config_values_atomic()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Modified Startup Flow in main.c
|
||||||
|
|
||||||
|
```c
|
||||||
|
// First-time startup
|
||||||
|
if (is_first_time_startup()) {
|
||||||
|
first_time_startup_sequence(&cli_options);
|
||||||
|
init_database(g_database_path);
|
||||||
|
populate_all_config_values_atomic(&cli_options); // NEW
|
||||||
|
apply_cli_overrides_atomic(&cli_options); // NEW
|
||||||
|
store_relay_private_key(relay_privkey);
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing relay
|
||||||
|
else {
|
||||||
|
startup_existing_relay(relay_pubkey);
|
||||||
|
init_database(g_database_path);
|
||||||
|
validate_config_table_completeness(); // NEW
|
||||||
|
if (has_cli_overrides(&cli_options)) {
|
||||||
|
apply_cli_overrides_atomic(&cli_options); // NEW
|
||||||
|
}
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Atomicity**: Config creation is atomic - no partial states
|
||||||
|
2. **Simplicity**: Clear phases with single responsibility
|
||||||
|
3. **Safety**: Cache only loaded after complete config exists
|
||||||
|
4. **Consistency**: Same validation logic for all scenarios
|
||||||
|
5. **Maintainability**: Easier to understand and modify
|
||||||
|
6. **Testability**: Each phase can be tested independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
1. Implement new functions in config.c
|
||||||
|
2. Update main.c startup flow
|
||||||
|
3. Test first-time startup scenario
|
||||||
|
4. Test restart with CLI overrides
|
||||||
|
5. Test restart without CLI overrides
|
||||||
|
6. Remove deprecated functions
|
||||||
|
7. Update documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Test Cases
|
||||||
|
|
||||||
|
1. **First-time startup with defaults**
|
||||||
|
- Verify all config values created atomically
|
||||||
|
- Verify cache loads complete config
|
||||||
|
|
||||||
|
2. **First-time startup with port override**
|
||||||
|
- Verify defaults created first
|
||||||
|
- Verify port override applied second
|
||||||
|
- Verify cache reflects override
|
||||||
|
|
||||||
|
3. **Restart with complete config**
|
||||||
|
- Verify no config changes
|
||||||
|
- Verify cache loads immediately
|
||||||
|
|
||||||
|
4. **Restart with missing config keys**
|
||||||
|
- Verify missing keys populated
|
||||||
|
- Verify existing keys unchanged
|
||||||
|
|
||||||
|
5. **Restart with CLI overrides**
|
||||||
|
- Verify overrides applied atomically
|
||||||
|
- Verify cache invalidated and refreshed
|
||||||
|
|
||||||
|
### Validation Points
|
||||||
|
|
||||||
|
- Config table row count after each phase
|
||||||
|
- Cache validity state after each phase
|
||||||
|
- Transaction boundaries (BEGIN/COMMIT)
|
||||||
|
- Error handling for failed transactions
|
||||||
746
docs/unified_startup_implementation_plan.md
Normal file
746
docs/unified_startup_implementation_plan.md
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
# Unified Startup Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document provides a detailed implementation plan for refactoring the startup sequence to use atomic config creation followed by CLI overrides. This plan breaks down the work into discrete, testable steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Create New Functions in config.c
|
||||||
|
|
||||||
|
### Step 1.1: Implement `populate_all_config_values_atomic()`
|
||||||
|
|
||||||
|
**Location**: `src/config.c`
|
||||||
|
|
||||||
|
**Purpose**: Create complete config table in single transaction for first-time startup
|
||||||
|
|
||||||
|
**Function Signature**:
|
||||||
|
```c
|
||||||
|
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
```c
|
||||||
|
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
|
||||||
|
if (!g_database) {
|
||||||
|
DEBUG_ERROR("Database not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
char* err_msg = NULL;
|
||||||
|
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare INSERT statement
|
||||||
|
sqlite3_stmt* stmt = NULL;
|
||||||
|
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert all default config values
|
||||||
|
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, DEFAULT_CONFIG_VALUES[i].key, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, DEFAULT_CONFIG_VALUES[i].value, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to insert config key '%s': %s",
|
||||||
|
DEFAULT_CONFIG_VALUES[i].key, sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert admin_pubkey from cache
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, g_unified_cache.admin_pubkey, -1, SQLITE_STATIC);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to insert admin_pubkey: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert relay_pubkey from cache
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, "relay_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, g_unified_cache.relay_pubkey, -1, SQLITE_STATIC);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to insert relay_pubkey: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Successfully populated all config values atomically");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify transaction atomicity (all or nothing)
|
||||||
|
- Verify all DEFAULT_CONFIG_VALUES inserted
|
||||||
|
- Verify admin_pubkey and relay_pubkey inserted
|
||||||
|
- Verify error handling on failure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1.2: Implement `apply_cli_overrides_atomic()`
|
||||||
|
|
||||||
|
**Location**: `src/config.c`
|
||||||
|
|
||||||
|
**Purpose**: Apply CLI overrides to existing config table in single transaction
|
||||||
|
|
||||||
|
**Function Signature**:
|
||||||
|
```c
|
||||||
|
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
```c
|
||||||
|
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
|
||||||
|
if (!g_database) {
|
||||||
|
DEBUG_ERROR("Database not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cli_options) {
|
||||||
|
DEBUG_ERROR("CLI options is NULL");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any overrides exist
|
||||||
|
bool has_overrides = false;
|
||||||
|
if (cli_options->port_override > 0) has_overrides = true;
|
||||||
|
if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = true;
|
||||||
|
if (cli_options->relay_privkey_override[0] != '\0') has_overrides = true;
|
||||||
|
|
||||||
|
if (!has_overrides) {
|
||||||
|
DEBUG_INFO("No CLI overrides to apply");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
char* err_msg = NULL;
|
||||||
|
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare UPDATE statement
|
||||||
|
sqlite3_stmt* stmt = NULL;
|
||||||
|
const char* sql = "UPDATE config SET value = ? WHERE key = ?";
|
||||||
|
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply port override
|
||||||
|
if (cli_options->port_override > 0) {
|
||||||
|
char port_str[16];
|
||||||
|
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
|
||||||
|
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, port_str, -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 2, "relay_port", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to update relay_port: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
DEBUG_INFO("Applied CLI override: relay_port = %s", port_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply admin_pubkey override
|
||||||
|
if (cli_options->admin_pubkey_override[0] != '\0') {
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, cli_options->admin_pubkey_override, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, "admin_pubkey", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to update admin_pubkey: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
DEBUG_INFO("Applied CLI override: admin_pubkey");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply relay_privkey override
|
||||||
|
if (cli_options->relay_privkey_override[0] != '\0') {
|
||||||
|
sqlite3_reset(stmt);
|
||||||
|
sqlite3_bind_text(stmt, 1, cli_options->relay_privkey_override, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, "relay_privkey", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to update relay_privkey: %s", sqlite3_errmsg(g_database));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
DEBUG_INFO("Applied CLI override: relay_privkey");
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache to force refresh
|
||||||
|
invalidate_config_cache();
|
||||||
|
|
||||||
|
DEBUG_INFO("Successfully applied CLI overrides atomically");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify transaction atomicity
|
||||||
|
- Verify each override type (port, admin_pubkey, relay_privkey)
|
||||||
|
- Verify cache invalidation after overrides
|
||||||
|
- Verify no-op when no overrides present
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1.3: Implement `validate_config_table_completeness()`
|
||||||
|
|
||||||
|
**Location**: `src/config.c`
|
||||||
|
|
||||||
|
**Purpose**: Validate config table has all required keys, populate missing ones
|
||||||
|
|
||||||
|
**Function Signature**:
|
||||||
|
```c
|
||||||
|
int validate_config_table_completeness(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
```c
|
||||||
|
int validate_config_table_completeness(void) {
|
||||||
|
if (!g_database) {
|
||||||
|
DEBUG_ERROR("Database not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Validating config table completeness");
|
||||||
|
|
||||||
|
// Check each default config key
|
||||||
|
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
|
||||||
|
const char* key = DEFAULT_CONFIG_VALUES[i].key;
|
||||||
|
|
||||||
|
// Check if key exists
|
||||||
|
sqlite3_stmt* stmt = NULL;
|
||||||
|
const char* sql = "SELECT COUNT(*) FROM config WHERE key = ?";
|
||||||
|
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
if (rc == SQLITE_ROW) {
|
||||||
|
count = sqlite3_column_int(stmt, 0);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
// If key missing, populate it
|
||||||
|
if (count == 0) {
|
||||||
|
DEBUG_WARN("Config key '%s' missing, populating with default", key);
|
||||||
|
rc = populate_missing_config_key(key, DEFAULT_CONFIG_VALUES[i].value);
|
||||||
|
if (rc != 0) {
|
||||||
|
DEBUG_ERROR("Failed to populate missing key '%s'", key);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Config table validation complete");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helper Function**:
|
||||||
|
```c
|
||||||
|
static int populate_missing_config_key(const char* key, const char* value) {
|
||||||
|
sqlite3_stmt* stmt = NULL;
|
||||||
|
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
|
||||||
|
|
||||||
|
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to insert config key '%s': %s", key, sqlite3_errmsg(g_database));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify detection of missing keys
|
||||||
|
- Verify population of missing keys with defaults
|
||||||
|
- Verify no changes when all keys present
|
||||||
|
- Verify error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1.4: Implement `has_cli_overrides()`
|
||||||
|
|
||||||
|
**Location**: `src/config.c`
|
||||||
|
|
||||||
|
**Purpose**: Check if any CLI overrides are present
|
||||||
|
|
||||||
|
**Function Signature**:
|
||||||
|
```c
|
||||||
|
bool has_cli_overrides(const cli_options_t* cli_options);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
```c
|
||||||
|
bool has_cli_overrides(const cli_options_t* cli_options) {
|
||||||
|
if (!cli_options) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cli_options->port_override > 0 ||
|
||||||
|
cli_options->admin_pubkey_override[0] != '\0' ||
|
||||||
|
cli_options->relay_privkey_override[0] != '\0');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify returns true when any override present
|
||||||
|
- Verify returns false when no overrides
|
||||||
|
- Verify NULL safety
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Update Function Declarations in config.h
|
||||||
|
|
||||||
|
### Step 2.1: Add New Function Declarations
|
||||||
|
|
||||||
|
**Location**: `src/config.h`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
```c
|
||||||
|
// Add after existing function declarations
|
||||||
|
|
||||||
|
// Atomic config population for first-time startup
|
||||||
|
int populate_all_config_values_atomic(const cli_options_t* cli_options);
|
||||||
|
|
||||||
|
// Atomic CLI override application
|
||||||
|
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
|
||||||
|
|
||||||
|
// Config validation for existing databases
|
||||||
|
int validate_config_table_completeness(void);
|
||||||
|
|
||||||
|
// Helper function to check for CLI overrides
|
||||||
|
bool has_cli_overrides(const cli_options_t* cli_options);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Refactor Startup Flow in main.c
|
||||||
|
|
||||||
|
### Step 3.1: Update First-Time Startup Branch
|
||||||
|
|
||||||
|
**Location**: `src/main.c` (around lines 1624-1740)
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```c
|
||||||
|
if (is_first_time_startup()) {
|
||||||
|
first_time_startup_sequence(&cli_options);
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// Current incremental approach
|
||||||
|
populate_default_config_values();
|
||||||
|
if (cli_options.port_override > 0) {
|
||||||
|
char port_str[16];
|
||||||
|
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
|
||||||
|
update_config_in_table("relay_port", port_str);
|
||||||
|
}
|
||||||
|
add_pubkeys_to_config_table();
|
||||||
|
|
||||||
|
store_relay_private_key(relay_privkey);
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code**:
|
||||||
|
```c
|
||||||
|
if (is_first_time_startup()) {
|
||||||
|
// 1. Generate keys and set database path
|
||||||
|
first_time_startup_sequence(&cli_options);
|
||||||
|
|
||||||
|
// 2. Create database with schema
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// 3. Populate ALL config values atomically (defaults + pubkeys)
|
||||||
|
if (populate_all_config_values_atomic(&cli_options) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to populate config values");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Apply CLI overrides atomically (separate transaction)
|
||||||
|
if (apply_cli_overrides_atomic(&cli_options) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to apply CLI overrides");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Store relay private key securely
|
||||||
|
store_relay_private_key(relay_privkey);
|
||||||
|
|
||||||
|
// 6. Load complete config into cache
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify first-time startup creates complete config
|
||||||
|
- Verify CLI overrides applied correctly
|
||||||
|
- Verify cache loads complete config
|
||||||
|
- Verify error handling at each step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3.2: Update Existing Relay Startup Branch
|
||||||
|
|
||||||
|
**Location**: `src/main.c` (around lines 1741-1928)
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```c
|
||||||
|
else {
|
||||||
|
char** existing_files = find_existing_db_files();
|
||||||
|
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||||
|
startup_existing_relay(relay_pubkey);
|
||||||
|
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// Current approach - unclear when overrides applied
|
||||||
|
populate_default_config_values();
|
||||||
|
if (cli_options.port_override > 0) {
|
||||||
|
// ... override logic ...
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code**:
|
||||||
|
```c
|
||||||
|
else {
|
||||||
|
// 1. Discover existing database
|
||||||
|
char** existing_files = find_existing_db_files();
|
||||||
|
if (!existing_files || !existing_files[0]) {
|
||||||
|
DEBUG_ERROR("No existing database files found");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
|
||||||
|
startup_existing_relay(relay_pubkey);
|
||||||
|
|
||||||
|
// 2. Open existing database
|
||||||
|
init_database(g_database_path);
|
||||||
|
|
||||||
|
// 3. Validate config table completeness (populate missing keys)
|
||||||
|
if (validate_config_table_completeness() != 0) {
|
||||||
|
DEBUG_ERROR("Failed to validate config table");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Apply CLI overrides if present (separate transaction)
|
||||||
|
if (has_cli_overrides(&cli_options)) {
|
||||||
|
if (apply_cli_overrides_atomic(&cli_options) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to apply CLI overrides");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Load complete config into cache
|
||||||
|
refresh_unified_cache_from_table();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- Verify existing relay startup with complete config
|
||||||
|
- Verify missing keys populated
|
||||||
|
- Verify CLI overrides applied when present
|
||||||
|
- Verify no changes when no overrides
|
||||||
|
- Verify cache loads correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Deprecate Old Functions
|
||||||
|
|
||||||
|
### Step 4.1: Mark Functions as Deprecated
|
||||||
|
|
||||||
|
**Location**: `src/config.c`
|
||||||
|
|
||||||
|
**Functions to Deprecate**:
|
||||||
|
1. `populate_default_config_values()` - replaced by `populate_all_config_values_atomic()`
|
||||||
|
2. `add_pubkeys_to_config_table()` - logic moved to `populate_all_config_values_atomic()`
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
```c
|
||||||
|
// Mark as deprecated in comments
|
||||||
|
// DEPRECATED: Use populate_all_config_values_atomic() instead
|
||||||
|
// This function will be removed in a future version
|
||||||
|
int populate_default_config_values(void) {
|
||||||
|
// ... existing implementation ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEPRECATED: Use populate_all_config_values_atomic() instead
|
||||||
|
// This function will be removed in a future version
|
||||||
|
int add_pubkeys_to_config_table(void) {
|
||||||
|
// ... existing implementation ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
1. **Test `populate_all_config_values_atomic()`**
|
||||||
|
- Test with valid cli_options
|
||||||
|
- Test transaction rollback on error
|
||||||
|
- Test all config keys inserted
|
||||||
|
- Test pubkeys inserted correctly
|
||||||
|
|
||||||
|
2. **Test `apply_cli_overrides_atomic()`**
|
||||||
|
- Test port override
|
||||||
|
- Test admin_pubkey override
|
||||||
|
- Test relay_privkey override
|
||||||
|
- Test multiple overrides
|
||||||
|
- Test no overrides
|
||||||
|
- Test transaction rollback on error
|
||||||
|
|
||||||
|
3. **Test `validate_config_table_completeness()`**
|
||||||
|
- Test with complete config
|
||||||
|
- Test with missing keys
|
||||||
|
- Test population of missing keys
|
||||||
|
|
||||||
|
4. **Test `has_cli_overrides()`**
|
||||||
|
- Test with each override type
|
||||||
|
- Test with no overrides
|
||||||
|
- Test with NULL cli_options
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
1. **First-Time Startup**
|
||||||
|
```bash
|
||||||
|
# Clean environment
|
||||||
|
rm -f *.db
|
||||||
|
|
||||||
|
# Start relay with defaults
|
||||||
|
./build/c_relay_x86
|
||||||
|
|
||||||
|
# Verify config table complete
|
||||||
|
sqlite3 <relay_pubkey>.db "SELECT COUNT(*) FROM config;"
|
||||||
|
# Expected: 20+ rows (all defaults + pubkeys)
|
||||||
|
|
||||||
|
# Verify cache loaded
|
||||||
|
# Check relay.log for cache refresh message
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **First-Time Startup with CLI Overrides**
|
||||||
|
```bash
|
||||||
|
# Clean environment
|
||||||
|
rm -f *.db
|
||||||
|
|
||||||
|
# Start relay with port override
|
||||||
|
./build/c_relay_x86 --port 9999
|
||||||
|
|
||||||
|
# Verify port override applied
|
||||||
|
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
|
||||||
|
# Expected: 9999
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restart with Existing Database**
|
||||||
|
```bash
|
||||||
|
# Start relay (creates database)
|
||||||
|
./build/c_relay_x86
|
||||||
|
|
||||||
|
# Stop relay
|
||||||
|
pkill -f c_relay_
|
||||||
|
|
||||||
|
# Restart relay
|
||||||
|
./build/c_relay_x86
|
||||||
|
|
||||||
|
# Verify config unchanged
|
||||||
|
# Check relay.log for validation message
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Restart with CLI Overrides**
|
||||||
|
```bash
|
||||||
|
# Start relay (creates database)
|
||||||
|
./build/c_relay_x86
|
||||||
|
|
||||||
|
# Stop relay
|
||||||
|
pkill -f c_relay_
|
||||||
|
|
||||||
|
# Restart with port override
|
||||||
|
./build/c_relay_x86 --port 9999
|
||||||
|
|
||||||
|
# Verify port override applied
|
||||||
|
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
|
||||||
|
# Expected: 9999
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regression Tests
|
||||||
|
|
||||||
|
Run existing test suite to ensure no breakage:
|
||||||
|
```bash
|
||||||
|
./tests/run_all_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Documentation Updates
|
||||||
|
|
||||||
|
### Files to Update
|
||||||
|
|
||||||
|
1. **docs/configuration_guide.md**
|
||||||
|
- Update startup sequence description
|
||||||
|
- Document new atomic config creation
|
||||||
|
- Document CLI override behavior
|
||||||
|
|
||||||
|
2. **docs/startup_flows_complete.md**
|
||||||
|
- Update with new flow diagrams
|
||||||
|
- Document new function calls
|
||||||
|
|
||||||
|
3. **README.md**
|
||||||
|
- Update CLI options documentation
|
||||||
|
- Document override behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
### Week 1: Core Functions
|
||||||
|
- Day 1-2: Implement `populate_all_config_values_atomic()`
|
||||||
|
- Day 3-4: Implement `apply_cli_overrides_atomic()`
|
||||||
|
- Day 5: Implement `validate_config_table_completeness()` and `has_cli_overrides()`
|
||||||
|
|
||||||
|
### Week 2: Integration
|
||||||
|
- Day 1-2: Update main.c startup flow
|
||||||
|
- Day 3-4: Testing and bug fixes
|
||||||
|
- Day 5: Documentation updates
|
||||||
|
|
||||||
|
### Week 3: Cleanup
|
||||||
|
- Day 1-2: Deprecate old functions
|
||||||
|
- Day 3-4: Final testing and validation
|
||||||
|
- Day 5: Code review and merge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
### Potential Issues
|
||||||
|
|
||||||
|
1. **Database Lock Contention**
|
||||||
|
- Risk: Multiple transactions could cause locks
|
||||||
|
- Mitigation: Use BEGIN IMMEDIATE for write transactions
|
||||||
|
|
||||||
|
2. **Cache Invalidation Timing**
|
||||||
|
- Risk: Cache could be read before overrides applied
|
||||||
|
- Mitigation: Invalidate cache immediately after overrides
|
||||||
|
|
||||||
|
3. **Backward Compatibility**
|
||||||
|
- Risk: Existing databases might have incomplete config
|
||||||
|
- Mitigation: `validate_config_table_completeness()` handles this
|
||||||
|
|
||||||
|
4. **Transaction Rollback**
|
||||||
|
- Risk: Partial config on error
|
||||||
|
- Mitigation: All operations in transactions with proper rollback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. ✅ All config values created atomically in first-time startup
|
||||||
|
2. ✅ CLI overrides applied in separate atomic transaction
|
||||||
|
3. ✅ Existing databases validated and missing keys populated
|
||||||
|
4. ✅ Cache only loaded after complete config exists
|
||||||
|
5. ✅ All existing tests pass
|
||||||
|
6. ✅ No race conditions in config creation
|
||||||
|
7. ✅ Clear separation between config creation and override phases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues arise during implementation:
|
||||||
|
|
||||||
|
1. **Revert main.c changes** - restore original startup flow
|
||||||
|
2. **Keep new functions** - they can coexist with old code
|
||||||
|
3. **Add feature flag** - allow toggling between old and new behavior
|
||||||
|
4. **Gradual migration** - enable new behavior per scenario
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Feature flag approach
|
||||||
|
#define USE_ATOMIC_CONFIG_CREATION 1
|
||||||
|
|
||||||
|
#if USE_ATOMIC_CONFIG_CREATION
|
||||||
|
// New atomic approach
|
||||||
|
populate_all_config_values_atomic(&cli_options);
|
||||||
|
apply_cli_overrides_atomic(&cli_options);
|
||||||
|
#else
|
||||||
|
// Old incremental approach
|
||||||
|
populate_default_config_values();
|
||||||
|
// ... existing code ...
|
||||||
|
#endif
|
||||||
|
```
|
||||||
@@ -6,6 +6,7 @@ Complete guide for deploying, configuring, and managing the C Nostr Relay with e
|
|||||||
|
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Web Admin Interface](#web-admin-interface)
|
||||||
- [Configuration Management](#configuration-management)
|
- [Configuration Management](#configuration-management)
|
||||||
- [Administration](#administration)
|
- [Administration](#administration)
|
||||||
- [Monitoring](#monitoring)
|
- [Monitoring](#monitoring)
|
||||||
@@ -43,7 +44,8 @@ Admin Public Key: 68394d08ab87f936a42ff2deb15a84fbdfbe0996ee0eb20cda064aae67328
|
|||||||
### 3. Connect Clients
|
### 3. Connect Clients
|
||||||
Your relay is now available at:
|
Your relay is now available at:
|
||||||
- **WebSocket**: `ws://localhost:8888`
|
- **WebSocket**: `ws://localhost:8888`
|
||||||
- **NIP-11 Info**: `http://localhost:8888`
|
- **NIP-11 Info**: `http://localhost:8888` (with `Accept: application/nostr+json` header)
|
||||||
|
- **Web Admin Interface**: `http://localhost:8888/api/` (serves embedded admin interface)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -211,6 +213,38 @@ Send this to your relay via WebSocket, and changes are applied immediately.
|
|||||||
| `nip40_expiration_filter` | Filter expired events | "true" | "true", "false" |
|
| `nip40_expiration_filter` | Filter expired events | "true" | "true", "false" |
|
||||||
| `nip40_expiration_grace_period` | Grace period (seconds) | "300" | 0-86400 |
|
| `nip40_expiration_grace_period` | Grace period (seconds) | "300" | 0-86400 |
|
||||||
|
|
||||||
|
## Web Admin Interface
|
||||||
|
|
||||||
|
The relay includes a built-in web-based administration interface that provides a user-friendly way to manage your relay without command-line tools.
|
||||||
|
|
||||||
|
### Accessing the Interface
|
||||||
|
|
||||||
|
1. **Open your browser** and navigate to: `http://localhost:8888/api/`
|
||||||
|
2. **Authenticate** using your Nostr identity (the admin interface uses NIP-42 authentication)
|
||||||
|
3. **Manage configuration** through the web interface
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Real-time Configuration**: View and edit all relay settings
|
||||||
|
- **Database Statistics**: Monitor event counts, storage usage, and performance metrics
|
||||||
|
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
|
||||||
|
- **Relay Connection Testing**: Verify WebSocket connectivity and NIP-11 information
|
||||||
|
- **Event-Based Updates**: All changes are applied as signed Nostr events
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
- The web interface requires NIP-42 authentication with your admin pubkey
|
||||||
|
- All configuration changes are cryptographically signed
|
||||||
|
- The interface serves embedded static files (no external dependencies)
|
||||||
|
- CORS headers are included for proper browser operation
|
||||||
|
|
||||||
|
### Browser Compatibility
|
||||||
|
|
||||||
|
The admin interface works with modern browsers that support:
|
||||||
|
- WebSocket connections
|
||||||
|
- ES6 JavaScript features
|
||||||
|
- Modern CSS Grid and Flexbox layouts
|
||||||
|
|
||||||
## Administration
|
## Administration
|
||||||
|
|
||||||
### Viewing Current Configuration
|
### Viewing Current Configuration
|
||||||
|
|||||||
128
embed_web_files.sh
Executable file
128
embed_web_files.sh
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to embed web files into C headers for the C-Relay admin interface
|
||||||
|
# Converts HTML, CSS, and JS files from api/ directory into C byte arrays
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Embedding web files into C headers..."
|
||||||
|
|
||||||
|
# Output directory for generated headers
|
||||||
|
OUTPUT_DIR="src"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Function to convert a file to C byte array
|
||||||
|
file_to_c_array() {
|
||||||
|
local input_file="$1"
|
||||||
|
local array_name="$2"
|
||||||
|
local output_file="$3"
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
local file_size=$(stat -c%s "$input_file" 2>/dev/null || stat -f%z "$input_file" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
echo "// Auto-generated from $input_file" >> "$output_file"
|
||||||
|
echo "static const unsigned char ${array_name}_data[] = {" >> "$output_file"
|
||||||
|
|
||||||
|
# Convert file to hex bytes
|
||||||
|
hexdump -v -e '1/1 "0x%02x,"' "$input_file" >> "$output_file"
|
||||||
|
|
||||||
|
echo "};" >> "$output_file"
|
||||||
|
echo "static const size_t ${array_name}_size = $file_size;" >> "$output_file"
|
||||||
|
echo "" >> "$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate the header file
|
||||||
|
HEADER_FILE="$OUTPUT_DIR/embedded_web_content.h"
|
||||||
|
echo "// Auto-generated embedded web content header" > "$HEADER_FILE"
|
||||||
|
echo "// Do not edit manually - generated by embed_web_files.sh" >> "$HEADER_FILE"
|
||||||
|
echo "" >> "$HEADER_FILE"
|
||||||
|
echo "#ifndef EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
|
||||||
|
echo "#define EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
|
||||||
|
echo "" >> "$HEADER_FILE"
|
||||||
|
echo "#include <stddef.h>" >> "$HEADER_FILE"
|
||||||
|
echo "" >> "$HEADER_FILE"
|
||||||
|
|
||||||
|
# Generate the C file
|
||||||
|
SOURCE_FILE="$OUTPUT_DIR/embedded_web_content.c"
|
||||||
|
echo "// Auto-generated embedded web content" > "$SOURCE_FILE"
|
||||||
|
echo "// Do not edit manually - generated by embed_web_files.sh" >> "$SOURCE_FILE"
|
||||||
|
echo "" >> "$SOURCE_FILE"
|
||||||
|
echo "#include \"embedded_web_content.h\"" >> "$SOURCE_FILE"
|
||||||
|
echo "#include <string.h>" >> "$SOURCE_FILE"
|
||||||
|
echo "" >> "$SOURCE_FILE"
|
||||||
|
|
||||||
|
# Process each web file
|
||||||
|
declare -A file_map
|
||||||
|
|
||||||
|
# Find all web files
|
||||||
|
for file in api/*.html api/*.css api/*.js; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
# Get filename without path
|
||||||
|
basename=$(basename "$file")
|
||||||
|
|
||||||
|
# Create C identifier from filename
|
||||||
|
c_name=$(echo "$basename" | sed 's/[^a-zA-Z0-9_]/_/g' | sed 's/^_//')
|
||||||
|
|
||||||
|
# Determine content type
|
||||||
|
case "$file" in
|
||||||
|
*.html) content_type="text/html" ;;
|
||||||
|
*.css) content_type="text/css" ;;
|
||||||
|
*.js) content_type="application/javascript" ;;
|
||||||
|
*) content_type="text/plain" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Processing $file -> ${c_name}"
|
||||||
|
|
||||||
|
# No extern declarations needed - data is accessed through get_embedded_file()
|
||||||
|
|
||||||
|
# Add to source
|
||||||
|
file_to_c_array "$file" "$c_name" "$SOURCE_FILE"
|
||||||
|
|
||||||
|
# Store mapping for lookup function
|
||||||
|
file_map["/$basename"]="$c_name:$content_type"
|
||||||
|
if [ "$basename" = "index.html" ]; then
|
||||||
|
file_map["/"]="$c_name:$content_type"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate lookup function
|
||||||
|
echo "// Embedded file lookup function" >> "$HEADER_FILE"
|
||||||
|
echo "typedef struct {" >> "$HEADER_FILE"
|
||||||
|
echo " const char *path;" >> "$HEADER_FILE"
|
||||||
|
echo " const unsigned char *data;" >> "$HEADER_FILE"
|
||||||
|
echo " size_t size;" >> "$HEADER_FILE"
|
||||||
|
echo " const char *content_type;" >> "$HEADER_FILE"
|
||||||
|
echo "} embedded_file_t;" >> "$HEADER_FILE"
|
||||||
|
echo "" >> "$HEADER_FILE"
|
||||||
|
echo "embedded_file_t *get_embedded_file(const char *path);" >> "$HEADER_FILE"
|
||||||
|
echo "" >> "$HEADER_FILE"
|
||||||
|
echo "#endif // EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
|
||||||
|
|
||||||
|
# Generate lookup function implementation
|
||||||
|
echo "// File mapping" >> "$SOURCE_FILE"
|
||||||
|
echo "static embedded_file_t embedded_files[] = {" >> "$SOURCE_FILE"
|
||||||
|
|
||||||
|
for path in "${!file_map[@]}"; do
|
||||||
|
entry="${file_map[$path]}"
|
||||||
|
c_name="${entry%:*}"
|
||||||
|
content_type="${entry#*:}"
|
||||||
|
echo " {\"$path\", ${c_name}_data, ${c_name}_size, \"$content_type\"}," >> "$SOURCE_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " {NULL, NULL, 0, NULL} // Sentinel" >> "$SOURCE_FILE"
|
||||||
|
echo "};" >> "$SOURCE_FILE"
|
||||||
|
echo "" >> "$SOURCE_FILE"
|
||||||
|
|
||||||
|
echo "embedded_file_t *get_embedded_file(const char *path) {" >> "$SOURCE_FILE"
|
||||||
|
echo " for (int i = 0; embedded_files[i].path != NULL; i++) {" >> "$SOURCE_FILE"
|
||||||
|
echo " if (strcmp(path, embedded_files[i].path) == 0) {" >> "$SOURCE_FILE"
|
||||||
|
echo " return &embedded_files[i];" >> "$SOURCE_FILE"
|
||||||
|
echo " }" >> "$SOURCE_FILE"
|
||||||
|
echo " }" >> "$SOURCE_FILE"
|
||||||
|
echo " return NULL;" >> "$SOURCE_FILE"
|
||||||
|
echo "}" >> "$SOURCE_FILE"
|
||||||
|
|
||||||
|
echo "Web file embedding complete. Generated:" >&2
|
||||||
|
echo " $HEADER_FILE" >&2
|
||||||
|
echo " $SOURCE_FILE" >&2
|
||||||
150
examples/deployment/static-builder.Dockerfile
Normal file
150
examples/deployment/static-builder.Dockerfile
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# MUSL-based fully static C-Relay builder
|
||||||
|
# Produces portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
FROM alpine:latest AS builder
|
||||||
|
|
||||||
|
# Add alternative mirrors and install build dependencies with retry
|
||||||
|
RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.22/main" > /etc/apk/repositories && \
|
||||||
|
echo "http://dl-cdn.alpinelinux.org/alpine/v3.22/community" >> /etc/apk/repositories && \
|
||||||
|
echo "http://mirror.leaseweb.com/alpine/v3.22/main" >> /etc/apk/repositories && \
|
||||||
|
echo "http://mirror.leaseweb.com/alpine/v3.22/community" >> /etc/apk/repositories && \
|
||||||
|
apk update --no-cache || (sleep 5 && apk update --no-cache) || (sleep 10 && apk update --no-cache)
|
||||||
|
|
||||||
|
# Install build dependencies with retry logic
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
pkgconfig \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static \
|
||||||
|
curl-dev \
|
||||||
|
curl-static \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-static \
|
||||||
|
linux-headers || \
|
||||||
|
(sleep 10 && apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
pkgconfig \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static \
|
||||||
|
curl-dev \
|
||||||
|
curl-static \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-static \
|
||||||
|
linux-headers)
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build zlib static (if needed)
|
||||||
|
RUN if [ ! -f /usr/lib/libz.a ]; then \
|
||||||
|
cd /tmp && \
|
||||||
|
wget https://zlib.net/zlib-1.3.1.tar.gz && \
|
||||||
|
tar xzf zlib-1.3.1.tar.gz && \
|
||||||
|
cd zlib-1.3.1 && \
|
||||||
|
./configure --static --prefix=/usr && \
|
||||||
|
make && make install; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build OpenSSL static
|
||||||
|
RUN cd /tmp && \
|
||||||
|
wget https://www.openssl.org/source/openssl-3.0.13.tar.gz && \
|
||||||
|
tar xzf openssl-3.0.13.tar.gz && \
|
||||||
|
cd openssl-3.0.13 && \
|
||||||
|
./Configure linux-x86_64 no-shared --prefix=/usr && \
|
||||||
|
make && make install_sw
|
||||||
|
|
||||||
|
# Build SQLite with JSON1 extension enabled
|
||||||
|
RUN cd /tmp && \
|
||||||
|
wget https://www.sqlite.org/2024/sqlite-autoconf-3460000.tar.gz && \
|
||||||
|
tar xzf sqlite-autoconf-3460000.tar.gz && \
|
||||||
|
cd sqlite-autoconf-3460000 && \
|
||||||
|
./configure \
|
||||||
|
--enable-static \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-json1 \
|
||||||
|
--enable-fts5 \
|
||||||
|
--prefix=/usr \
|
||||||
|
CFLAGS="-DSQLITE_ENABLE_JSON1=1 -DSQLITE_ENABLE_FTS5=1" && \
|
||||||
|
make && make install
|
||||||
|
|
||||||
|
# Build libsecp256k1 static
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr && \
|
||||||
|
make && make install
|
||||||
|
|
||||||
|
# Build libwebsockets static with OpenSSL
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/warmcat/libwebsockets.git && \
|
||||||
|
cd libwebsockets && \
|
||||||
|
mkdir build && cd build && \
|
||||||
|
cmake .. \
|
||||||
|
-DLWS_WITH_STATIC=ON \
|
||||||
|
-DLWS_WITH_SHARED=OFF \
|
||||||
|
-DLWS_WITH_SSL=ON \
|
||||||
|
-DLWS_OPENSSL_LIBRARIES="/usr/lib/libssl.a;/usr/lib/libcrypto.a" \
|
||||||
|
-DLWS_OPENSSL_INCLUDE_DIRS="/usr/include" \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr && \
|
||||||
|
make && make install
|
||||||
|
|
||||||
|
# Build curl static (minimal features)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
wget https://curl.se/download/curl-8.6.0.tar.gz && \
|
||||||
|
tar xzf curl-8.6.0.tar.gz && \
|
||||||
|
cd curl-8.6.0 && \
|
||||||
|
./configure \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--disable-ldap \
|
||||||
|
--without-libidn2 \
|
||||||
|
--without-brotli \
|
||||||
|
--without-zstd \
|
||||||
|
--without-rtmp \
|
||||||
|
--without-libpsl \
|
||||||
|
--without-krb5 \
|
||||||
|
--with-openssl \
|
||||||
|
--prefix=/usr && \
|
||||||
|
make && make install
|
||||||
|
|
||||||
|
# Copy c-relay source
|
||||||
|
COPY . /build/
|
||||||
|
|
||||||
|
# Initialize submodules
|
||||||
|
RUN git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Build nostr_core_lib
|
||||||
|
RUN cd nostr_core_lib && ./build.sh
|
||||||
|
|
||||||
|
# Build c-relay static
|
||||||
|
RUN make clean && \
|
||||||
|
CC="musl-gcc -static" \
|
||||||
|
CFLAGS="-O2 -Wall -Wextra -std=c99 -g" \
|
||||||
|
LDFLAGS="-static -Wl,--whole-archive -lpthread -Wl,--no-whole-archive" \
|
||||||
|
LIBS="-lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -lsecp256k1 -lssl -lcrypto -lcurl" \
|
||||||
|
make
|
||||||
|
|
||||||
|
# Strip binary for size
|
||||||
|
RUN strip build/c_relay_x86
|
||||||
|
|
||||||
|
# Multi-stage build to produce minimal output
|
||||||
|
FROM scratch AS output
|
||||||
|
COPY --from=builder /build/build/c_relay_x86 /c_relay_static_musl_x86_64
|
||||||
@@ -17,6 +17,29 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|||||||
COMMIT_MESSAGE=""
|
COMMIT_MESSAGE=""
|
||||||
RELEASE_MODE=false
|
RELEASE_MODE=false
|
||||||
|
|
||||||
|
show_usage() {
|
||||||
|
echo "C-Relay Increment and Push Script"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " $0 \"commit message\" - Default: increment patch, commit & push"
|
||||||
|
echo " $0 -r \"commit message\" - Release: increment minor, create release"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 \"Fixed event validation bug\""
|
||||||
|
echo " $0 --release \"Major release with new features\""
|
||||||
|
echo ""
|
||||||
|
echo "Default Mode (patch increment):"
|
||||||
|
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
||||||
|
echo " - Git add, commit with message, and push"
|
||||||
|
echo ""
|
||||||
|
echo "Release Mode (-r flag):"
|
||||||
|
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||||
|
echo " - Git add, commit, push, and create Gitea release"
|
||||||
|
echo ""
|
||||||
|
echo "Requirements for Release Mode:"
|
||||||
|
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||||
|
}
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
@@ -38,32 +61,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
show_usage() {
|
|
||||||
echo "C-Relay Build and Push Script"
|
|
||||||
echo ""
|
|
||||||
echo "Usage:"
|
|
||||||
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
|
|
||||||
echo " $0 -r \"commit message\" - Release: compile x86+arm64, increment minor, create release"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 \"Fixed event validation bug\""
|
|
||||||
echo " $0 --release \"Major release with new features\""
|
|
||||||
echo ""
|
|
||||||
echo "Default Mode (patch increment):"
|
|
||||||
echo " - Compile C-Relay"
|
|
||||||
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
|
||||||
echo " - Git add, commit with message, and push"
|
|
||||||
echo ""
|
|
||||||
echo "Release Mode (-r flag):"
|
|
||||||
echo " - Compile C-Relay for x86_64 and arm64"
|
|
||||||
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
|
||||||
echo " - Git add, commit, push, and create Gitea release"
|
|
||||||
echo ""
|
|
||||||
echo "Requirements for Release Mode:"
|
|
||||||
echo " - For ARM64 builds: make install-arm64-deps (optional - will build x86_64 only if missing)"
|
|
||||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||||
print_error "Commit message is required"
|
print_error "Commit message is required"
|
||||||
@@ -128,73 +125,6 @@ increment_version() {
|
|||||||
export NEW_VERSION
|
export NEW_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to compile the C-Relay project
|
|
||||||
compile_project() {
|
|
||||||
print_status "Compiling C-Relay..."
|
|
||||||
|
|
||||||
# Clean previous build
|
|
||||||
if make clean > /dev/null 2>&1; then
|
|
||||||
print_success "Cleaned previous build"
|
|
||||||
else
|
|
||||||
print_warning "Clean failed or no Makefile found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Force regenerate version.h to pick up new tags
|
|
||||||
if make force-version > /dev/null 2>&1; then
|
|
||||||
print_success "Regenerated version.h"
|
|
||||||
else
|
|
||||||
print_warning "Failed to regenerate version.h"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compile the project
|
|
||||||
if make > /dev/null 2>&1; then
|
|
||||||
print_success "C-Relay compiled successfully"
|
|
||||||
else
|
|
||||||
print_error "Compilation failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to build release binaries
|
|
||||||
build_release_binaries() {
|
|
||||||
print_status "Building release binaries..."
|
|
||||||
|
|
||||||
# Build x86_64 version
|
|
||||||
print_status "Building x86_64 version..."
|
|
||||||
make clean > /dev/null 2>&1
|
|
||||||
if make x86 > /dev/null 2>&1; then
|
|
||||||
if [[ -f "build/c_relay_x86" ]]; then
|
|
||||||
cp build/c_relay_x86 c-relay-x86_64
|
|
||||||
print_success "x86_64 binary created: c-relay-x86_64"
|
|
||||||
else
|
|
||||||
print_error "x86_64 binary not found after compilation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_error "x86_64 build failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try to build ARM64 version
|
|
||||||
print_status "Attempting ARM64 build..."
|
|
||||||
make clean > /dev/null 2>&1
|
|
||||||
if make arm64 > /dev/null 2>&1; then
|
|
||||||
if [[ -f "build/c_relay_arm64" ]]; then
|
|
||||||
cp build/c_relay_arm64 c-relay-arm64
|
|
||||||
print_success "ARM64 binary created: c-relay-arm64"
|
|
||||||
else
|
|
||||||
print_warning "ARM64 binary not found after compilation"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_warning "ARM64 build failed - ARM64 cross-compilation not properly set up"
|
|
||||||
print_status "Only x86_64 binary will be included in release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore normal build
|
|
||||||
make clean > /dev/null 2>&1
|
|
||||||
make > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to commit and push changes
|
# Function to commit and push changes
|
||||||
git_commit_and_push() {
|
git_commit_and_push() {
|
||||||
print_status "Preparing git commit..."
|
print_status "Preparing git commit..."
|
||||||
@@ -321,10 +251,10 @@ create_gitea_release() {
|
|||||||
|
|
||||||
if echo "$response" | grep -q '"id"'; then
|
if echo "$response" | grep -q '"id"'; then
|
||||||
print_success "Created release $NEW_VERSION"
|
print_success "Created release $NEW_VERSION"
|
||||||
upload_release_binaries "$api_url" "$token"
|
return 0
|
||||||
elif echo "$response" | grep -q "already exists"; then
|
elif echo "$response" | grep -q "already exists"; then
|
||||||
print_warning "Release $NEW_VERSION already exists"
|
print_warning "Release $NEW_VERSION already exists"
|
||||||
upload_release_binaries "$api_url" "$token"
|
return 0
|
||||||
else
|
else
|
||||||
print_error "Failed to create release $NEW_VERSION"
|
print_error "Failed to create release $NEW_VERSION"
|
||||||
print_error "Response: $response"
|
print_error "Response: $response"
|
||||||
@@ -334,7 +264,7 @@ create_gitea_release() {
|
|||||||
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||||
if echo "$check_response" | grep -q '"id"'; then
|
if echo "$check_response" | grep -q '"id"'; then
|
||||||
print_warning "Release exists but creation response was unexpected"
|
print_warning "Release exists but creation response was unexpected"
|
||||||
upload_release_binaries "$api_url" "$token"
|
return 0
|
||||||
else
|
else
|
||||||
print_error "Release does not exist and creation failed"
|
print_error "Release does not exist and creation failed"
|
||||||
return 1
|
return 1
|
||||||
@@ -342,68 +272,9 @@ create_gitea_release() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to upload release binaries
|
|
||||||
upload_release_binaries() {
|
|
||||||
local api_url="$1"
|
|
||||||
local token="$2"
|
|
||||||
|
|
||||||
# Get release ID with more robust parsing
|
|
||||||
print_status "Getting release ID for $NEW_VERSION..."
|
|
||||||
local response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
|
||||||
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
|
||||||
|
|
||||||
if [[ -z "$release_id" ]]; then
|
|
||||||
print_error "Could not get release ID for $NEW_VERSION"
|
|
||||||
print_error "API Response: $response"
|
|
||||||
|
|
||||||
# Try to list all releases to debug
|
|
||||||
print_status "Available releases:"
|
|
||||||
curl -s -H "Authorization: token $token" "$api_url/releases" | grep -o '"tag_name":"[^"]*"' | head -5
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Found release ID: $release_id"
|
|
||||||
|
|
||||||
# Upload x86_64 binary
|
|
||||||
if [[ -f "c-relay-x86_64" ]]; then
|
|
||||||
print_status "Uploading x86_64 binary..."
|
|
||||||
if curl -s -X POST "$api_url/releases/$release_id/assets" \
|
|
||||||
-H "Authorization: token $token" \
|
|
||||||
-F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64" > /dev/null; then
|
|
||||||
print_success "Uploaded x86_64 binary"
|
|
||||||
else
|
|
||||||
print_warning "Failed to upload x86_64 binary"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload ARM64 binary
|
|
||||||
if [[ -f "c-relay-arm64" ]]; then
|
|
||||||
print_status "Uploading ARM64 binary..."
|
|
||||||
if curl -s -X POST "$api_url/releases/$release_id/assets" \
|
|
||||||
-H "Authorization: token $token" \
|
|
||||||
-F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64" > /dev/null; then
|
|
||||||
print_success "Uploaded ARM64 binary"
|
|
||||||
else
|
|
||||||
print_warning "Failed to upload ARM64 binary"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to clean up release binaries
|
|
||||||
cleanup_release_binaries() {
|
|
||||||
if [[ -f "c-relay-x86_64" ]]; then
|
|
||||||
rm -f c-relay-x86_64
|
|
||||||
print_status "Cleaned up x86_64 binary"
|
|
||||||
fi
|
|
||||||
if [[ -f "c-relay-arm64" ]]; then
|
|
||||||
rm -f c-relay-arm64
|
|
||||||
print_status "Cleaned up ARM64 binary"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
main() {
|
main() {
|
||||||
print_status "C-Relay Build and Push Script"
|
print_status "C-Relay Increment and Push Script"
|
||||||
|
|
||||||
# Check prerequisites
|
# Check prerequisites
|
||||||
check_git_repo
|
check_git_repo
|
||||||
@@ -423,23 +294,15 @@ main() {
|
|||||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Compile project first (will now pick up the new tag)
|
|
||||||
compile_project
|
|
||||||
|
|
||||||
# Build release binaries
|
|
||||||
build_release_binaries
|
|
||||||
|
|
||||||
# Commit and push (but skip tag creation since we already did it)
|
# Commit and push (but skip tag creation since we already did it)
|
||||||
git_commit_and_push_no_tag
|
git_commit_and_push_no_tag
|
||||||
|
|
||||||
# Create Gitea release with binaries
|
# Create Gitea release
|
||||||
create_gitea_release
|
if create_gitea_release; then
|
||||||
|
print_success "Release $NEW_VERSION completed successfully!"
|
||||||
# Cleanup
|
else
|
||||||
cleanup_release_binaries
|
print_error "Release creation failed"
|
||||||
|
fi
|
||||||
print_success "Release $NEW_VERSION completed successfully!"
|
|
||||||
print_status "Binaries uploaded to Gitea release"
|
|
||||||
|
|
||||||
else
|
else
|
||||||
print_status "=== DEFAULT MODE ==="
|
print_status "=== DEFAULT MODE ==="
|
||||||
@@ -456,13 +319,10 @@ main() {
|
|||||||
git tag "$NEW_VERSION" > /dev/null 2>&1
|
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Compile project (will now pick up the new tag)
|
|
||||||
compile_project
|
|
||||||
|
|
||||||
# Commit and push (but skip tag creation since we already did it)
|
# Commit and push (but skip tag creation since we already did it)
|
||||||
git_commit_and_push_no_tag
|
git_commit_and_push_no_tag
|
||||||
|
|
||||||
print_success "Build and push completed successfully!"
|
print_success "Increment and push completed successfully!"
|
||||||
print_status "Version $NEW_VERSION pushed to repository"
|
print_status "Version $NEW_VERSION pushed to repository"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,98 @@ echo "=== C Nostr Relay Build and Restart Script ==="
|
|||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
PRESERVE_DATABASE=false
|
PRESERVE_DATABASE=false
|
||||||
HELP=false
|
HELP=false
|
||||||
|
USE_TEST_KEYS=false
|
||||||
|
ADMIN_KEY=""
|
||||||
|
RELAY_KEY=""
|
||||||
|
PORT_OVERRIDE=""
|
||||||
|
DEBUG_LEVEL="5"
|
||||||
|
|
||||||
|
# Key validation function
|
||||||
|
validate_hex_key() {
|
||||||
|
local key="$1"
|
||||||
|
local key_type="$2"
|
||||||
|
|
||||||
|
if [ ${#key} -ne 64 ]; then
|
||||||
|
echo "ERROR: $key_type key must be exactly 64 characters"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "$key" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
||||||
|
echo "ERROR: $key_type key must contain only hex characters (0-9, a-f, A-F)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--preserve-database|-p)
|
-a|--admin-key)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "ERROR: Admin key option requires a value"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
ADMIN_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-r|--relay-key)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "ERROR: Relay key option requires a value"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
RELAY_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-p|--port)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "ERROR: Port option requires a value"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
PORT_OVERRIDE="$2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-d|--preserve-database)
|
||||||
PRESERVE_DATABASE=true
|
PRESERVE_DATABASE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--test-keys|-t)
|
||||||
|
USE_TEST_KEYS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug-level=*)
|
||||||
|
DEBUG_LEVEL="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-d=*)
|
||||||
|
DEBUG_LEVEL="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug-level)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "ERROR: Debug level option requires a value"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
DEBUG_LEVEL="$2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-d)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "ERROR: Debug level option requires a value"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
DEBUG_LEVEL="$2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
HELP=true
|
HELP=true
|
||||||
shift
|
shift
|
||||||
@@ -27,13 +112,47 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Validate custom keys if provided
|
||||||
|
if [ -n "$ADMIN_KEY" ]; then
|
||||||
|
if ! validate_hex_key "$ADMIN_KEY" "Admin"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELAY_KEY" ]; then
|
||||||
|
if ! validate_hex_key "$RELAY_KEY" "Relay"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate port if provided
|
||||||
|
if [ -n "$PORT_OVERRIDE" ]; then
|
||||||
|
if ! [[ "$PORT_OVERRIDE" =~ ^[0-9]+$ ]] || [ "$PORT_OVERRIDE" -lt 1 ] || [ "$PORT_OVERRIDE" -gt 65535 ]; then
|
||||||
|
echo "ERROR: Port must be a number between 1 and 65535"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate debug level if provided
|
||||||
|
if [ -n "$DEBUG_LEVEL" ]; then
|
||||||
|
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
|
||||||
|
echo "ERROR: Debug level must be 0-5, got: $DEBUG_LEVEL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Show help
|
# Show help
|
||||||
if [ "$HELP" = true ]; then
|
if [ "$HELP" = true ]; then
|
||||||
echo "Usage: $0 [OPTIONS]"
|
echo "Usage: $0 [OPTIONS]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --preserve-database, -p Keep existing database files (don't delete for fresh start)"
|
echo " -a, --admin-key <hex> 64-character hex admin private key"
|
||||||
echo " --help, -h Show this help message"
|
echo " -r, --relay-key <hex> 64-character hex relay private key"
|
||||||
|
echo " -p, --port <port> Custom port override (default: 8888)"
|
||||||
|
echo " -d, --debug-level <0-5> Set debug level: 0=none, 1=errors, 2=warnings, 3=info, 4=debug, 5=trace"
|
||||||
|
echo " --preserve-database Keep existing database files (don't delete for fresh start)"
|
||||||
|
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
|
||||||
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Event-Based Configuration:"
|
echo "Event-Based Configuration:"
|
||||||
echo " This relay now uses event-based configuration stored directly in the database."
|
echo " This relay now uses event-based configuration stored directly in the database."
|
||||||
@@ -41,9 +160,16 @@ if [ "$HELP" = true ]; then
|
|||||||
echo " Database file: <relay_pubkey>.db (created automatically)"
|
echo " Database file: <relay_pubkey>.db (created automatically)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Fresh start with new keys (default)"
|
echo " $0 # Fresh start with random keys"
|
||||||
echo " $0 -p # Preserve existing database and keys"
|
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
||||||
|
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
||||||
|
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
|
||||||
|
echo " $0 -d=5 # Start with debug level 5 (trace)"
|
||||||
|
echo " $0 --preserve-database # Preserve existing database and keys"
|
||||||
|
echo " $0 --test-keys # Use test keys for consistent development"
|
||||||
|
echo " $0 -t --preserve-database # Use test keys and preserve database"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Key Format: Keys must be exactly 64 hexadecimal characters (0-9, a-f, A-F)"
|
||||||
echo "Default behavior: Deletes existing database files to start fresh with new keys"
|
echo "Default behavior: Deletes existing database files to start fresh with new keys"
|
||||||
echo " for development purposes"
|
echo " for development purposes"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -51,38 +177,34 @@ fi
|
|||||||
|
|
||||||
# Handle database file cleanup for fresh start
|
# Handle database file cleanup for fresh start
|
||||||
if [ "$PRESERVE_DATABASE" = false ]; then
|
if [ "$PRESERVE_DATABASE" = false ]; then
|
||||||
if ls *.db >/dev/null 2>&1 || ls build/*.db >/dev/null 2>&1; then
|
if ls *.db* >/dev/null 2>&1 || ls build/*.db* >/dev/null 2>&1; then
|
||||||
echo "Removing existing database files to trigger fresh key generation..."
|
echo "Removing existing database files (including WAL/SHM) to trigger fresh key generation..."
|
||||||
rm -f *.db build/*.db
|
rm -f *.db* build/*.db*
|
||||||
echo "✓ Database files removed - will generate new keys and database"
|
echo "✓ Database files removed - will generate new keys and database"
|
||||||
else
|
else
|
||||||
echo "No existing database found - will generate fresh setup"
|
echo "No existing database found - will generate fresh setup"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Preserving existing database files as requested"
|
echo "Preserving existing database files (build process does not touch database files)"
|
||||||
# Back up database files before clean build
|
|
||||||
if ls build/*.db >/dev/null 2>&1; then
|
|
||||||
echo "Backing up existing database files..."
|
|
||||||
mkdir -p /tmp/relay_backup_$$
|
|
||||||
cp build/*.db* /tmp/relay_backup_$$/ 2>/dev/null || true
|
|
||||||
echo "Database files backed up to temporary location"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up legacy files that are no longer used
|
# Clean up legacy files that are no longer used
|
||||||
rm -rf dev-config/ 2>/dev/null
|
rm -rf dev-config/ 2>/dev/null
|
||||||
rm -f db/c_nostr_relay.db* 2>/dev/null
|
rm -f db/c_nostr_relay.db* 2>/dev/null
|
||||||
|
|
||||||
# Build the project first
|
# Embed web files into C headers before building
|
||||||
echo "Building project..."
|
echo "Embedding web files..."
|
||||||
make clean all
|
./embed_web_files.sh
|
||||||
|
|
||||||
# Restore database files if preserving
|
# Build the project - ONLY static build
|
||||||
if [ "$PRESERVE_DATABASE" = true ] && [ -d "/tmp/relay_backup_$$" ]; then
|
echo "Building project (static binary with SQLite JSON1 extension)..."
|
||||||
echo "Restoring preserved database files..."
|
./build_static.sh
|
||||||
cp /tmp/relay_backup_$$/*.db* build/ 2>/dev/null || true
|
|
||||||
rm -rf /tmp/relay_backup_$$
|
# Exit if static build fails - no fallback
|
||||||
echo "Database files restored to build directory"
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "ERROR: Static build failed. Cannot proceed without static binary."
|
||||||
|
echo "Please fix the build errors and try again."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if build was successful
|
# Check if build was successful
|
||||||
@@ -91,46 +213,82 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if relay binary exists after build - detect architecture
|
# Check if static relay binary exists after build - ONLY use static binary
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
x86_64)
|
x86_64)
|
||||||
BINARY_PATH="./build/c_relay_x86"
|
BINARY_PATH="./build/c_relay_static_x86_64"
|
||||||
;;
|
;;
|
||||||
aarch64|arm64)
|
aarch64|arm64)
|
||||||
BINARY_PATH="./build/c_relay_arm64"
|
BINARY_PATH="./build/c_relay_static_arm64"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
BINARY_PATH="./build/c_relay_$ARCH"
|
BINARY_PATH="./build/c_relay_static_$ARCH"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Verify static binary exists - no fallbacks
|
||||||
if [ ! -f "$BINARY_PATH" ]; then
|
if [ ! -f "$BINARY_PATH" ]; then
|
||||||
echo "ERROR: Relay binary not found at $BINARY_PATH after build. Build may have failed."
|
echo "ERROR: Static relay binary not found: $BINARY_PATH"
|
||||||
|
echo ""
|
||||||
|
echo "The relay requires the static binary with JSON1 support."
|
||||||
|
echo "Please run: ./build_static.sh"
|
||||||
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Using static binary: $BINARY_PATH"
|
||||||
|
|
||||||
echo "Build successful. Proceeding with relay restart..."
|
echo "Build successful. Proceeding with relay restart..."
|
||||||
|
|
||||||
# Kill existing relay if running
|
# Kill existing relay if running - start aggressive immediately
|
||||||
echo "Stopping any existing relay servers..."
|
echo "Stopping any existing relay servers..."
|
||||||
pkill -f "c_relay_" 2>/dev/null
|
|
||||||
sleep 2 # Give time for shutdown
|
|
||||||
|
|
||||||
# Check if port is still bound
|
# Get all relay processes and kill them immediately with -9
|
||||||
if lsof -i :8888 >/dev/null 2>&1; then
|
RELAY_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||||
echo "Port 8888 still in use, force killing..."
|
if [ -n "$RELAY_PIDS" ]; then
|
||||||
fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888"
|
echo "Force killing relay processes immediately: $RELAY_PIDS"
|
||||||
|
kill -9 $RELAY_PIDS 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "No existing relay processes found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get any remaining processes
|
# Ensure port 8888 is completely free with retry loop
|
||||||
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
echo "Ensuring port 8888 is available..."
|
||||||
if [ -n "$REMAINING_PIDS" ]; then
|
for attempt in {1..15}; do
|
||||||
echo "Force killing remaining processes: $REMAINING_PIDS"
|
if ! lsof -i :8888 >/dev/null 2>&1; then
|
||||||
kill -9 $REMAINING_PIDS 2>/dev/null
|
echo "Port 8888 is now free"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Attempt $attempt: Port 8888 still in use, force killing..."
|
||||||
|
# Kill anything using port 8888
|
||||||
|
fuser -k 8888/tcp 2>/dev/null || true
|
||||||
|
|
||||||
|
# Double-check for any remaining relay processes
|
||||||
|
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||||
|
if [ -n "$REMAINING_PIDS" ]; then
|
||||||
|
echo "Killing remaining relay processes: $REMAINING_PIDS"
|
||||||
|
kill -9 $REMAINING_PIDS 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if [ $attempt -eq 15 ]; then
|
||||||
|
echo "ERROR: Could not free port 8888 after 15 attempts"
|
||||||
|
echo "Current processes using port:"
|
||||||
|
lsof -i :8888 2>/dev/null || echo "No process details available"
|
||||||
|
echo "You may need to manually kill processes or reboot"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Final safety check - ensure no relay processes remain
|
||||||
|
FINAL_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||||
|
if [ -n "$FINAL_PIDS" ]; then
|
||||||
|
echo "Final cleanup: killing processes $FINAL_PIDS"
|
||||||
|
kill -9 $FINAL_PIDS 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
else
|
|
||||||
echo "No existing relay found"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up PID file
|
# Clean up PID file
|
||||||
@@ -144,10 +302,43 @@ echo "Database will be initialized automatically on startup if needed"
|
|||||||
echo "Starting relay server..."
|
echo "Starting relay server..."
|
||||||
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
|
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
|
||||||
|
|
||||||
|
# Build command line arguments for relay binary
|
||||||
|
RELAY_ARGS=""
|
||||||
|
|
||||||
|
if [ -n "$ADMIN_KEY" ]; then
|
||||||
|
RELAY_ARGS="$RELAY_ARGS -a $ADMIN_KEY"
|
||||||
|
echo "Using custom admin key: ${ADMIN_KEY:0:16}..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELAY_KEY" ]; then
|
||||||
|
RELAY_ARGS="$RELAY_ARGS -r $RELAY_KEY"
|
||||||
|
echo "Using custom relay key: ${RELAY_KEY:0:16}..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PORT_OVERRIDE" ]; then
|
||||||
|
RELAY_ARGS="$RELAY_ARGS -p $PORT_OVERRIDE"
|
||||||
|
echo "Using custom port: $PORT_OVERRIDE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$DEBUG_LEVEL" ]; then
|
||||||
|
RELAY_ARGS="$RELAY_ARGS --debug-level=$DEBUG_LEVEL"
|
||||||
|
echo "Using debug level: $DEBUG_LEVEL"
|
||||||
|
fi
|
||||||
|
|
||||||
# Change to build directory before starting relay so database files are created there
|
# Change to build directory before starting relay so database files are created there
|
||||||
cd build
|
cd build
|
||||||
# Start relay in background and capture its PID (no command line arguments needed)
|
# Start relay in background and capture its PID
|
||||||
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
|
if [ "$USE_TEST_KEYS" = true ]; then
|
||||||
|
echo "Using deterministic test keys for development..."
|
||||||
|
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||||
|
elif [ -n "$RELAY_ARGS" ]; then
|
||||||
|
echo "Starting relay with custom configuration..."
|
||||||
|
./$(basename $BINARY_PATH) $RELAY_ARGS --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||||
|
else
|
||||||
|
# No command line arguments needed for random key generation
|
||||||
|
echo "Starting relay with random key generation..."
|
||||||
|
./$(basename $BINARY_PATH) --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
|
||||||
|
fi
|
||||||
RELAY_PID=$!
|
RELAY_PID=$!
|
||||||
# Change back to original directory
|
# Change back to original directory
|
||||||
cd ..
|
cd ..
|
||||||
@@ -161,7 +352,34 @@ sleep 3
|
|||||||
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
||||||
echo "Relay started successfully!"
|
echo "Relay started successfully!"
|
||||||
echo "PID: $RELAY_PID"
|
echo "PID: $RELAY_PID"
|
||||||
echo "WebSocket endpoint: ws://127.0.0.1:8888"
|
|
||||||
|
# Wait for relay to fully initialize and detect the actual port it's using
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Extract actual port from relay logs
|
||||||
|
ACTUAL_PORT=""
|
||||||
|
if [ -f relay.log ]; then
|
||||||
|
# Look for the success message with actual port
|
||||||
|
ACTUAL_PORT=$(grep "WebSocket relay started on ws://127.0.0.1:" relay.log 2>/dev/null | tail -1 | sed -n 's/.*ws:\/\/127\.0\.0\.1:\([0-9]*\).*/\1/p')
|
||||||
|
|
||||||
|
# If we couldn't find the port in logs, try to detect from netstat
|
||||||
|
if [ -z "$ACTUAL_PORT" ]; then
|
||||||
|
ACTUAL_PORT=$(netstat -tln 2>/dev/null | grep -E ":888[0-9]" | head -1 | sed -n 's/.*:\([0-9]*\).*/\1/p')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Display the actual endpoint
|
||||||
|
if [ -n "$ACTUAL_PORT" ]; then
|
||||||
|
if [ "$ACTUAL_PORT" = "8888" ]; then
|
||||||
|
echo "WebSocket endpoint: ws://127.0.0.1:$ACTUAL_PORT"
|
||||||
|
else
|
||||||
|
echo "WebSocket endpoint: ws://127.0.0.1:$ACTUAL_PORT (fell back from port 8888)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WebSocket endpoint: ws://127.0.0.1:8888 (port detection failed - check logs)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "HTTP endpoint: http://127.0.0.1:${ACTUAL_PORT:-8888}"
|
||||||
echo "Log file: relay.log"
|
echo "Log file: relay.log"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
3
nip_11_curl.sh
Executable file
3
nip_11_curl.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
curl -H "Accept: application/nostr+json" http://localhost:8888/
|
||||||
153
node_modules/.package-lock.json
generated
vendored
Normal file
153
node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"name": "c-relay",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/@noble/ciphers": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/base": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "~1.1.0",
|
||||||
|
"@noble/hashes": "~1.3.1",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "~1.3.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools": {
|
||||||
|
"version": "2.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz",
|
||||||
|
"integrity": "sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "^0.5.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@scure/base": "1.1.1",
|
||||||
|
"@scure/bip32": "1.3.1",
|
||||||
|
"@scure/bip39": "1.2.1",
|
||||||
|
"nostr-wasm": "0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
node_modules/@noble/ciphers/LICENSE
generated
vendored
Normal file
22
node_modules/@noble/ciphers/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Paul Miller (https://paulmillr.com)
|
||||||
|
Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
||||||
|
|
||||||
|
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.
|
||||||
655
node_modules/@noble/ciphers/README.md
generated
vendored
Normal file
655
node_modules/@noble/ciphers/README.md
generated
vendored
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
# noble-ciphers
|
||||||
|
|
||||||
|
Auditable & minimal JS implementation of Salsa20, ChaCha and AES.
|
||||||
|
|
||||||
|
- 🔒 Auditable
|
||||||
|
- 🔻 Tree-shaking-friendly: use only what's necessary, other code won't be included
|
||||||
|
- 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines
|
||||||
|
- 🔍 Unique tests ensure correctness: property-based, cross-library and Wycheproof vectors
|
||||||
|
- 💼 AES: ECB, CBC, CTR, CFB, GCM, SIV (nonce misuse-resistant)
|
||||||
|
- 💃 Salsa20, ChaCha, XSalsa20, XChaCha, Poly1305, ChaCha8, ChaCha12
|
||||||
|
- 🥈 Two AES implementations: choose between friendly webcrypto wrapper and pure JS one
|
||||||
|
- 🪶 45KB (8KB gzipped) for everything, 10KB (3KB gzipped) for ChaCha build
|
||||||
|
|
||||||
|
For discussions, questions and support, visit
|
||||||
|
[GitHub Discussions](https://github.com/paulmillr/noble-ciphers/discussions)
|
||||||
|
section of the repository.
|
||||||
|
|
||||||
|
### This library belongs to _noble_ cryptography
|
||||||
|
|
||||||
|
> **noble cryptography** — high-security, easily auditable set of contained cryptographic libraries and tools.
|
||||||
|
|
||||||
|
- Zero or minimal dependencies
|
||||||
|
- Highly readable TypeScript / JS code
|
||||||
|
- PGP-signed releases and transparent NPM builds
|
||||||
|
- All libraries:
|
||||||
|
[ciphers](https://github.com/paulmillr/noble-ciphers),
|
||||||
|
[curves](https://github.com/paulmillr/noble-curves),
|
||||||
|
[hashes](https://github.com/paulmillr/noble-hashes),
|
||||||
|
[post-quantum](https://github.com/paulmillr/noble-post-quantum),
|
||||||
|
4kb [secp256k1](https://github.com/paulmillr/noble-secp256k1) /
|
||||||
|
[ed25519](https://github.com/paulmillr/noble-ed25519)
|
||||||
|
- [Check out homepage](https://paulmillr.com/noble/)
|
||||||
|
for reading resources, documentation and apps built with noble
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> npm install @noble/ciphers
|
||||||
|
|
||||||
|
We support all major platforms and runtimes.
|
||||||
|
For [Deno](https://deno.land), ensure to use
|
||||||
|
[npm specifier](https://deno.land/manual@v1.28.0/node/npm_specifiers).
|
||||||
|
For React Native, you may need a
|
||||||
|
[polyfill for getRandomValues](https://github.com/LinusU/react-native-get-random-values).
|
||||||
|
A standalone file
|
||||||
|
[noble-ciphers.js](https://github.com/paulmillr/noble-ciphers/releases) is also available.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// import * from '@noble/ciphers'; // Error: use sub-imports, to ensure small app size
|
||||||
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
// import { xchacha20poly1305 } from 'npm:@noble/ciphers@0.5.0/chacha'; // Deno
|
||||||
|
```
|
||||||
|
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Encrypt with XChaCha20-Poly1305](#encrypt-with-xchacha20-poly1305)
|
||||||
|
- [Encrypt with AES-256-GCM](#encrypt-with-aes-256-gcm)
|
||||||
|
- [Use existing key instead of a new one](#use-existing-key-instead-of-a-new-one)
|
||||||
|
- [Encrypt without nonce](#encrypt-without-nonce)
|
||||||
|
- [Use same array for input and output](#use-same-array-for-input-and-output)
|
||||||
|
- [All imports](#all-imports)
|
||||||
|
- [Implementations](#implementations)
|
||||||
|
- [Salsa20](#salsa)
|
||||||
|
- [ChaCha](#chacha)
|
||||||
|
- [AES](#aes)
|
||||||
|
- [Webcrypto AES](#webcrypto-aes)
|
||||||
|
- [Poly1305, GHash, Polyval](#poly1305-ghash-polyval)
|
||||||
|
- [FF1 format-preserving encryption](#ff1)
|
||||||
|
- [Managed nonces](#managed-nonces)
|
||||||
|
- [Guidance](#guidance)
|
||||||
|
- [Which cipher should I pick?](#which-cipher-should-i-pick)
|
||||||
|
- [How to encrypt properly](#how-to-encrypt-properly)
|
||||||
|
- [Nonces](#nonces)
|
||||||
|
- [Encryption limits](#encryption-limits)
|
||||||
|
- [AES internals and block modes](#aes-internals-and-block-modes)
|
||||||
|
- [Security](#security)
|
||||||
|
- [Speed](#speed)
|
||||||
|
- [Upgrading](#upgrading)
|
||||||
|
- [Contributing & testing](#contributing--testing)
|
||||||
|
- [Resources](#resources)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
#### Encrypt with XChaCha20-Poly1305
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
import { utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
const key = randomBytes(32);
|
||||||
|
const nonce = randomBytes(24);
|
||||||
|
const chacha = xchacha20poly1305(key, nonce);
|
||||||
|
const data = utf8ToBytes('hello, noble');
|
||||||
|
const ciphertext = chacha.encrypt(data);
|
||||||
|
const data_ = chacha.decrypt(ciphertext); // utils.bytesToUtf8(data_) === data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Encrypt with AES-256-GCM
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { gcm } from '@noble/ciphers/aes';
|
||||||
|
import { utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
const key = randomBytes(32);
|
||||||
|
const nonce = randomBytes(24);
|
||||||
|
const aes = gcm(key, nonce);
|
||||||
|
const data = utf8ToBytes('hello, noble');
|
||||||
|
const ciphertext = aes.encrypt(data);
|
||||||
|
const data_ = aes.decrypt(ciphertext); // utils.bytesToUtf8(data_) === data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use existing key instead of a new one
|
||||||
|
|
||||||
|
```js
|
||||||
|
const key = new Uint8Array([
|
||||||
|
169, 88, 160, 139, 168, 29, 147, 196, 14, 88, 237, 76, 243, 177, 109, 140, 195, 140, 80, 10, 216,
|
||||||
|
134, 215, 71, 191, 48, 20, 104, 189, 37, 38, 55,
|
||||||
|
]);
|
||||||
|
const nonce = new Uint8Array([
|
||||||
|
180, 90, 27, 63, 160, 191, 150, 33, 67, 212, 86, 71, 144, 6, 200, 102, 218, 32, 23, 147, 8, 41,
|
||||||
|
147, 11,
|
||||||
|
]);
|
||||||
|
// or, hex:
|
||||||
|
import { hexToBytes } from '@noble/ciphers/utils';
|
||||||
|
const key2 = hexToBytes('4b7f89bac90a1086fef73f5da2cbe93b2fae9dfbf7678ae1f3e75fd118ddf999');
|
||||||
|
const nonce2 = hexToBytes('9610467513de0bbd7c4cc2c3c64069f1802086fbd3232b13');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Encrypt without nonce
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
import { managedNonce } from '@noble/ciphers/webcrypto';
|
||||||
|
import { hexToBytes, utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
const key = hexToBytes('fa686bfdffd3758f6377abbc23bf3d9bdc1a0dda4a6e7f8dbdd579fa1ff6d7e1');
|
||||||
|
const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
|
||||||
|
const data = utf8ToBytes('hello, noble');
|
||||||
|
const ciphertext = chacha.encrypt(data);
|
||||||
|
const data_ = chacha.decrypt(ciphertext);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use same array for input and output
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { chacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
import { utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
|
||||||
|
const key = randomBytes(32);
|
||||||
|
const nonce = randomBytes(12);
|
||||||
|
const buf = new Uint8Array(12 + 16);
|
||||||
|
const _data = utf8ToBytes('hello, noble');
|
||||||
|
buf.set(_data, 0); // first 12 bytes
|
||||||
|
const _12b = buf.subarray(0, 12);
|
||||||
|
|
||||||
|
const chacha = chacha20poly1305(key, nonce);
|
||||||
|
chacha.encrypt(_12b, buf);
|
||||||
|
chacha.decrypt(buf, _12b); // _12b now same as _data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### All imports
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { gcm, siv } from '@noble/ciphers/aes';
|
||||||
|
import { xsalsa20poly1305 } from '@noble/ciphers/salsa';
|
||||||
|
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
|
||||||
|
// Unauthenticated encryption: make sure to use HMAC or similar
|
||||||
|
import { ctr, cfb, cbc, ecb } from '@noble/ciphers/aes';
|
||||||
|
import { salsa20, xsalsa20 } from '@noble/ciphers/salsa';
|
||||||
|
import { chacha20, xchacha20, chacha8, chacha12 } from '@noble/ciphers/chacha';
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
import { bytesToHex, hexToBytes, bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
import { managedNonce, randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
### Salsa
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { xsalsa20poly1305 } from '@noble/ciphers/salsa';
|
||||||
|
import { secretbox } from '@noble/ciphers/salsa'; // == xsalsa20poly1305
|
||||||
|
import { salsa20, xsalsa20 } from '@noble/ciphers/salsa';
|
||||||
|
```
|
||||||
|
|
||||||
|
[Salsa20](https://cr.yp.to/snuffle.html) stream cipher was released in 2005.
|
||||||
|
Salsa's goal was to implement AES replacement that does not rely on S-Boxes,
|
||||||
|
which are hard to implement in a constant-time manner.
|
||||||
|
Salsa20 is usually faster than AES, a big deal on slow, budget mobile phones.
|
||||||
|
|
||||||
|
[XSalsa20](https://cr.yp.to/snuffle/xsalsa-20110204.pdf), extended-nonce
|
||||||
|
variant was released in 2008. It switched nonces from 96-bit to 192-bit,
|
||||||
|
and became safe to be picked at random.
|
||||||
|
|
||||||
|
Nacl / Libsodium popularized term "secretbox", a simple black-box
|
||||||
|
authenticated encryption. Secretbox is just xsalsa20-poly1305. We provide the
|
||||||
|
alias and corresponding seal / open methods. We don't provide "box" or "sealedbox".
|
||||||
|
|
||||||
|
Check out [PDF](https://cr.yp.to/snuffle/salsafamily-20071225.pdf) and
|
||||||
|
[wiki](https://en.wikipedia.org/wiki/Salsa20).
|
||||||
|
|
||||||
|
### ChaCha
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
import { chacha20, xchacha20, chacha8, chacha12 } from '@noble/ciphers/chacha';
|
||||||
|
```
|
||||||
|
|
||||||
|
[ChaCha20](https://cr.yp.to/chacha.html) stream cipher was released
|
||||||
|
in 2008. ChaCha aims to increase the diffusion per round, but had slightly less
|
||||||
|
cryptanalysis. It was standardized in
|
||||||
|
[RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) and is now used in TLS 1.3.
|
||||||
|
|
||||||
|
[XChaCha20](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha)
|
||||||
|
extended-nonce variant is also provided. Similar to XSalsa, it's safe to use with
|
||||||
|
randomly-generated nonces.
|
||||||
|
|
||||||
|
Check out [PDF](http://cr.yp.to/chacha/chacha-20080128.pdf) and [wiki](https://en.wikipedia.org/wiki/Salsa20).
|
||||||
|
|
||||||
|
### AES
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { gcm, siv, ctr, cfb, cbc, ecb } from '@noble/ciphers/aes';
|
||||||
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
const plaintext = new Uint8Array(32).fill(16);
|
||||||
|
const key = randomBytes(32); // 24 for AES-192, 16 for AES-128
|
||||||
|
for (let cipher of [gcm, siv]) {
|
||||||
|
const stream = cipher(key, randomBytes(12));
|
||||||
|
const ciphertext_ = stream.encrypt(plaintext);
|
||||||
|
const plaintext_ = stream.decrypt(ciphertext_);
|
||||||
|
}
|
||||||
|
for (const cipher of [ctr, cbc, cbc]) {
|
||||||
|
const stream = cipher(key, randomBytes(16));
|
||||||
|
const ciphertext_ = stream.encrypt(plaintext);
|
||||||
|
const plaintext_ = stream.decrypt(ciphertext_);
|
||||||
|
}
|
||||||
|
for (const cipher of [ecb]) {
|
||||||
|
const stream = cipher(key);
|
||||||
|
const ciphertext_ = stream.encrypt(plaintext);
|
||||||
|
const plaintext_ = stream.decrypt(ciphertext_);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
|
||||||
|
is a variant of Rijndael block cipher, standardized by NIST in 2001.
|
||||||
|
We provide the fastest available pure JS implementation.
|
||||||
|
|
||||||
|
We support AES-128, AES-192 and AES-256: the mode is selected dynamically,
|
||||||
|
based on key length (16, 24, 32).
|
||||||
|
|
||||||
|
[AES-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV)
|
||||||
|
nonce-misuse-resistant mode is also provided. It's recommended to use it,
|
||||||
|
to prevent catastrophic consequences of nonce reuse. Our implementation of SIV
|
||||||
|
has the same speed as GCM: there is no performance hit.
|
||||||
|
|
||||||
|
Check out [AES internals and block modes](#aes-internals-and-block-modes).
|
||||||
|
|
||||||
|
### Webcrypto AES
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { gcm, ctr, cbc, randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
const plaintext = new Uint8Array(32).fill(16);
|
||||||
|
const key = randomBytes(32);
|
||||||
|
for (const cipher of [gcm]) {
|
||||||
|
const stream = cipher(key, randomBytes(12));
|
||||||
|
const ciphertext_ = await stream.encrypt(plaintext);
|
||||||
|
const plaintext_ = await stream.decrypt(ciphertext_);
|
||||||
|
}
|
||||||
|
for (const cipher of [ctr, cbc]) {
|
||||||
|
const stream = cipher(key, randomBytes(16));
|
||||||
|
const ciphertext_ = await stream.encrypt(plaintext);
|
||||||
|
const plaintext_ = await stream.decrypt(ciphertext_);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We also have a separate wrapper over WebCrypto built-in.
|
||||||
|
|
||||||
|
It's the same as using `crypto.subtle`, but with massively simplified API.
|
||||||
|
|
||||||
|
Unlike pure js version, it's asynchronous.
|
||||||
|
|
||||||
|
### Poly1305, GHash, Polyval
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { poly1305 } from '@noble/ciphers/_poly1305';
|
||||||
|
import { ghash, polyval } from '@noble/ciphers/_polyval';
|
||||||
|
```
|
||||||
|
|
||||||
|
We expose polynomial-evaluation MACs: [Poly1305](https://cr.yp.to/mac.html),
|
||||||
|
AES-GCM's [GHash](https://en.wikipedia.org/wiki/Galois/Counter_Mode) and
|
||||||
|
AES-SIV's [Polyval](https://en.wikipedia.org/wiki/AES-GCM-SIV).
|
||||||
|
|
||||||
|
Poly1305 ([PDF](https://cr.yp.to/mac/poly1305-20050329.pdf),
|
||||||
|
[wiki](https://en.wikipedia.org/wiki/Poly1305))
|
||||||
|
is a fast and parallel secret-key message-authentication code suitable for
|
||||||
|
a wide variety of applications. It was standardized in
|
||||||
|
[RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) and is now used in TLS 1.3.
|
||||||
|
|
||||||
|
Polynomial MACs are not perfect for every situation:
|
||||||
|
they lack Random Key Robustness: the MAC can be forged, and can't
|
||||||
|
be used in PAKE schemes. See
|
||||||
|
[invisible salamanders attack](https://keymaterial.net/2020/09/07/invisible-salamanders-in-aes-gcm-siv/).
|
||||||
|
To combat invisible salamanders, `hash(key)` can be included in ciphertext,
|
||||||
|
however, this would violate ciphertext indistinguishability:
|
||||||
|
an attacker would know which key was used - so `HKDF(key, i)`
|
||||||
|
could be used instead.
|
||||||
|
|
||||||
|
### FF1
|
||||||
|
|
||||||
|
Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
|
||||||
|
[See more info](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf).
|
||||||
|
|
||||||
|
### Managed nonces
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { managedNonce } from '@noble/ciphers/webcrypto';
|
||||||
|
import { gcm, siv, ctr, cbc, cbc, ecb } from '@noble/ciphers/aes';
|
||||||
|
import { xsalsa20poly1305 } from '@noble/ciphers/salsa';
|
||||||
|
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
|
|
||||||
|
const wgcm = managedNonce(gcm);
|
||||||
|
const wsiv = managedNonce(siv);
|
||||||
|
const wcbc = managedNonce(cbc);
|
||||||
|
const wctr = managedNonce(ctr);
|
||||||
|
const wsalsapoly = managedNonce(xsalsa20poly1305);
|
||||||
|
const wchacha = managedNonce(chacha20poly1305);
|
||||||
|
const wxchacha = managedNonce(xchacha20poly1305);
|
||||||
|
|
||||||
|
// Now:
|
||||||
|
const encrypted = wgcm(key).encrypt(data); // no nonces
|
||||||
|
```
|
||||||
|
|
||||||
|
We provide API that manages nonce internally instead of exposing them to library's user.
|
||||||
|
|
||||||
|
For `encrypt`, a `nonceBytes`-length buffer is fetched from CSPRNG and prenended to encrypted ciphertext.
|
||||||
|
|
||||||
|
For `decrypt`, first `nonceBytes` of ciphertext are treated as nonce.
|
||||||
|
|
||||||
|
## Guidance
|
||||||
|
|
||||||
|
### Which cipher should I pick?
|
||||||
|
|
||||||
|
XChaCha20-Poly1305 is the safest bet these days.
|
||||||
|
AES-GCM-SIV is the second safest.
|
||||||
|
AES-GCM is the third.
|
||||||
|
|
||||||
|
### How to encrypt properly
|
||||||
|
|
||||||
|
- Use unpredictable key with enough entropy
|
||||||
|
- Random key must be using cryptographically secure random number generator (CSPRNG), not `Math.random` etc.
|
||||||
|
- Non-random key generated from KDF is fine
|
||||||
|
- Re-using key is fine, but be aware of rules for cryptographic key wear-out and [encryption limits](#encryption-limits)
|
||||||
|
- Use new nonce every time and [don't repeat it](#nonces)
|
||||||
|
- chacha and salsa20 are fine for sequential counters that _never_ repeat: `01, 02...`
|
||||||
|
- xchacha and xsalsa20 should be used for random nonces instead
|
||||||
|
- Prefer authenticated encryption (AEAD)
|
||||||
|
- HMAC+ChaCha / HMAC+AES / chacha20poly1305 / aes-gcm is good
|
||||||
|
- chacha20 without poly1305 or hmac / aes-ctr / aes-cbc is bad
|
||||||
|
- Flipping bits or ciphertext substitution won't be detected in unauthenticated ciphers
|
||||||
|
- Don't re-use keys between different protocols
|
||||||
|
- For example, using secp256k1 key in AES is bad
|
||||||
|
- Use hkdf or, at least, a hash function to create sub-key instead
|
||||||
|
|
||||||
|
### Nonces
|
||||||
|
|
||||||
|
Most ciphers need a key and a nonce (aka initialization vector / IV) to encrypt a data:
|
||||||
|
|
||||||
|
ciphertext = encrypt(plaintext, key, nonce)
|
||||||
|
|
||||||
|
Repeating (key, nonce) pair with different plaintexts would allow an attacker to decrypt it:
|
||||||
|
|
||||||
|
ciphertext_a = encrypt(plaintext_a, key, nonce)
|
||||||
|
ciphertext_b = encrypt(plaintext_b, key, nonce)
|
||||||
|
stream_diff = xor(ciphertext_a, ciphertext_b) # Break encryption
|
||||||
|
|
||||||
|
So, you can't repeat nonces. One way of doing so is using counters:
|
||||||
|
|
||||||
|
for i in 0..:
|
||||||
|
ciphertext[i] = encrypt(plaintexts[i], key, i)
|
||||||
|
|
||||||
|
Another is generating random nonce every time:
|
||||||
|
|
||||||
|
for i in 0..:
|
||||||
|
rand_nonces[i] = random()
|
||||||
|
ciphertext[i] = encrypt(plaintexts[i], key, rand_nonces[i])
|
||||||
|
|
||||||
|
Counters are OK, but it's not always possible to store current counter value:
|
||||||
|
e.g. in decentralized, unsyncable systems.
|
||||||
|
|
||||||
|
Randomness is OK, but there's a catch:
|
||||||
|
ChaCha20 and AES-GCM use 96-bit / 12-byte nonces, which implies
|
||||||
|
higher chance of collision. In the example above,
|
||||||
|
`random()` can collide and produce repeating nonce.
|
||||||
|
|
||||||
|
To safely use random nonces, utilize XSalsa20 or XChaCha:
|
||||||
|
they increased nonce length to 192-bit, minimizing a chance of collision.
|
||||||
|
AES-SIV is also fine. In situations where you can't use eXtended-nonce
|
||||||
|
algorithms, key rotation is advised. hkdf would work great for this case.
|
||||||
|
|
||||||
|
### Encryption limits
|
||||||
|
|
||||||
|
A "protected message" would mean a probability of `2**-50` that a passive attacker
|
||||||
|
successfully distinguishes the ciphertext outputs of the AEAD scheme from the outputs
|
||||||
|
of a random function. See [draft-irtf-cfrg-aead-limits](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/) for details.
|
||||||
|
|
||||||
|
- Max message size:
|
||||||
|
- AES-GCM: ~68GB, `2**36-256`
|
||||||
|
- Salsa, ChaCha, XSalsa, XChaCha: ~256GB, `2**38-64`
|
||||||
|
- Max amount of protected messages, under same key:
|
||||||
|
- AES-GCM: `2**32.5`
|
||||||
|
- Salsa, ChaCha: `2**46`, but only integrity is affected, not confidentiality
|
||||||
|
- XSalsa, XChaCha: `2**72`
|
||||||
|
- Max amount of protected messages, across all keys:
|
||||||
|
- AES-GCM: `2**69/B` where B is max blocks encrypted by a key. Meaning
|
||||||
|
`2**59` for 1KB, `2**49` for 1MB, `2**39` for 1GB
|
||||||
|
- Salsa, ChaCha, XSalsa, XChaCha: `2**100`
|
||||||
|
|
||||||
|
##### AES internals and block modes
|
||||||
|
|
||||||
|
`cipher = encrypt(block, key)`. Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256bit). Every round does:
|
||||||
|
|
||||||
|
1. **S-box**, table substitution
|
||||||
|
2. **Shift rows**, cyclic shift left of all rows of data array
|
||||||
|
3. **Mix columns**, multiplying every column by fixed polynomial
|
||||||
|
4. **Add round key**, round_key xor i-th column of array
|
||||||
|
|
||||||
|
For non-deterministic (not ECB) schemes, initialization vector (IV) is mixed to block/key;
|
||||||
|
and each new round either depends on previous block's key, or on some counter.
|
||||||
|
|
||||||
|
- ECB — simple deterministic replacement. Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/)
|
||||||
|
- CBC — key is previous round’s block. Hard to use: need proper padding, also needs MAC
|
||||||
|
- CTR — counter, allows to create streaming cipher. Requires good IV. Parallelizable. OK, but no MAC
|
||||||
|
- GCM — modern CTR, parallel, with MAC
|
||||||
|
- SIV — synthetic initialization vector, nonce-misuse-resistant. Guarantees that, when a nonce is repeated,
|
||||||
|
the only security loss is that identical plaintexts will produce identical ciphertexts.
|
||||||
|
- XTS — used in hard drives. Similar to ECB (deterministic), but has `[i][j]`
|
||||||
|
tweak arguments corresponding to sector i and 16-byte block (part of sector) j. Not authenticated!
|
||||||
|
|
||||||
|
GCM / SIV are not ideal:
|
||||||
|
|
||||||
|
- Conservative key wear-out is `2**32` (4B) msgs
|
||||||
|
- MAC can be forged: see Poly1305 section above. Same for SIV
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The library has not been independently audited yet.
|
||||||
|
|
||||||
|
It is tested against property-based, cross-library and Wycheproof vectors,
|
||||||
|
and has fuzzing by [Guido Vranken's cryptofuzz](https://github.com/guidovranken/cryptofuzz).
|
||||||
|
|
||||||
|
If you see anything unusual: investigate and report.
|
||||||
|
|
||||||
|
### Constant-timeness
|
||||||
|
|
||||||
|
_JIT-compiler_ and _Garbage Collector_ make "constant time" extremely hard to
|
||||||
|
achieve [timing attack](https://en.wikipedia.org/wiki/Timing_attack) resistance
|
||||||
|
in a scripting language. Which means _any other JS library can't have
|
||||||
|
constant-timeness_. Even statically typed Rust, a language without GC,
|
||||||
|
[makes it harder to achieve constant-time](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security)
|
||||||
|
for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones.
|
||||||
|
Use low-level libraries & languages. Nonetheless we're targetting algorithmic constant time.
|
||||||
|
|
||||||
|
AES uses T-tables, which means it can't be done in constant-time in JS.
|
||||||
|
|
||||||
|
### Supply chain security
|
||||||
|
|
||||||
|
- **Commits** are signed with PGP keys, to prevent forgery. Make sure to verify commit signatures.
|
||||||
|
- **Releases** are transparent and built on GitHub CI. Make sure to verify [provenance](https://docs.npmjs.com/generating-provenance-statements) logs
|
||||||
|
- **Rare releasing** is followed to ensure less re-audit need for end-users
|
||||||
|
- **Dependencies** are minimized and locked-down:
|
||||||
|
- If your app has 500 dependencies, any dep could get hacked and you'll be downloading
|
||||||
|
malware with every install. We make sure to use as few dependencies as possible
|
||||||
|
- We prevent automatic dependency updates by locking-down version ranges. Every update is checked with `npm-diff`
|
||||||
|
- **Dev Dependencies** are only used if you want to contribute to the repo. They are disabled for end-users:
|
||||||
|
- scure-base, micro-bmark and micro-should are developed by the same author and follow identical security practices
|
||||||
|
- prettier (linter), fast-check (property-based testing) and typescript are used for code quality, vector generation and ts compilation. The packages are big, which makes it hard to audit their source code thoroughly and fully
|
||||||
|
|
||||||
|
### Randomness
|
||||||
|
|
||||||
|
We're deferring to built-in
|
||||||
|
[crypto.getRandomValues](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)
|
||||||
|
which is considered cryptographically secure (CSPRNG).
|
||||||
|
|
||||||
|
In the past, browsers had bugs that made it weak: it may happen again.
|
||||||
|
Implementing a userspace CSPRNG to get resilient to the weakness
|
||||||
|
is even worse: there is no reliable userspace source of quality entropy.
|
||||||
|
|
||||||
|
## Speed
|
||||||
|
|
||||||
|
To summarize, noble is the fastest JS implementation of Salsa, ChaCha and AES.
|
||||||
|
|
||||||
|
You can gain additional speed-up and
|
||||||
|
avoid memory allocations by passing `output`
|
||||||
|
uint8array into encrypt / decrypt methods.
|
||||||
|
|
||||||
|
Benchmark results on Apple M2 with node v20:
|
||||||
|
|
||||||
|
```
|
||||||
|
encrypt (64B)
|
||||||
|
├─xsalsa20poly1305 x 485,672 ops/sec @ 2μs/op
|
||||||
|
├─chacha20poly1305 x 466,200 ops/sec @ 2μs/op
|
||||||
|
├─xchacha20poly1305 x 312,500 ops/sec @ 3μs/op
|
||||||
|
├─aes-256-gcm x 151,057 ops/sec @ 6μs/op
|
||||||
|
└─aes-256-gcm-siv x 124,984 ops/sec @ 8μs/op
|
||||||
|
encrypt (1KB)
|
||||||
|
├─xsalsa20poly1305 x 146,477 ops/sec @ 6μs/op
|
||||||
|
├─chacha20poly1305 x 145,518 ops/sec @ 6μs/op
|
||||||
|
├─xchacha20poly1305 x 126,119 ops/sec @ 7μs/op
|
||||||
|
├─aes-256-gcm x 43,207 ops/sec @ 23μs/op
|
||||||
|
└─aes-256-gcm-siv x 39,363 ops/sec @ 25μs/op
|
||||||
|
encrypt (8KB)
|
||||||
|
├─xsalsa20poly1305 x 23,773 ops/sec @ 42μs/op
|
||||||
|
├─chacha20poly1305 x 24,134 ops/sec @ 41μs/op
|
||||||
|
├─xchacha20poly1305 x 23,520 ops/sec @ 42μs/op
|
||||||
|
├─aes-256-gcm x 8,420 ops/sec @ 118μs/op
|
||||||
|
└─aes-256-gcm-siv x 8,126 ops/sec @ 123μs/op
|
||||||
|
encrypt (1MB)
|
||||||
|
├─xsalsa20poly1305 x 195 ops/sec @ 5ms/op
|
||||||
|
├─chacha20poly1305 x 199 ops/sec @ 5ms/op
|
||||||
|
├─xchacha20poly1305 x 198 ops/sec @ 5ms/op
|
||||||
|
├─aes-256-gcm x 76 ops/sec @ 13ms/op
|
||||||
|
└─aes-256-gcm-siv x 78 ops/sec @ 12ms/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Unauthenticated encryption:
|
||||||
|
|
||||||
|
```
|
||||||
|
encrypt (64B)
|
||||||
|
├─salsa x 1,287,001 ops/sec @ 777ns/op
|
||||||
|
├─chacha x 1,555,209 ops/sec @ 643ns/op
|
||||||
|
├─xsalsa x 938,086 ops/sec @ 1μs/op
|
||||||
|
└─xchacha x 920,810 ops/sec @ 1μs/op
|
||||||
|
encrypt (1KB)
|
||||||
|
├─salsa x 353,107 ops/sec @ 2μs/op
|
||||||
|
├─chacha x 377,216 ops/sec @ 2μs/op
|
||||||
|
├─xsalsa x 331,674 ops/sec @ 3μs/op
|
||||||
|
└─xchacha x 336,247 ops/sec @ 2μs/op
|
||||||
|
encrypt (8KB)
|
||||||
|
├─salsa x 57,084 ops/sec @ 17μs/op
|
||||||
|
├─chacha x 59,520 ops/sec @ 16μs/op
|
||||||
|
├─xsalsa x 57,097 ops/sec @ 17μs/op
|
||||||
|
└─xchacha x 58,278 ops/sec @ 17μs/op
|
||||||
|
encrypt (1MB)
|
||||||
|
├─salsa x 479 ops/sec @ 2ms/op
|
||||||
|
├─chacha x 491 ops/sec @ 2ms/op
|
||||||
|
├─xsalsa x 483 ops/sec @ 2ms/op
|
||||||
|
└─xchacha x 492 ops/sec @ 2ms/op
|
||||||
|
|
||||||
|
AES
|
||||||
|
encrypt (64B)
|
||||||
|
├─ctr-256 x 689,179 ops/sec @ 1μs/op
|
||||||
|
├─cbc-256 x 639,795 ops/sec @ 1μs/op
|
||||||
|
└─ecb-256 x 668,449 ops/sec @ 1μs/op
|
||||||
|
encrypt (1KB)
|
||||||
|
├─ctr-256 x 93,668 ops/sec @ 10μs/op
|
||||||
|
├─cbc-256 x 94,428 ops/sec @ 10μs/op
|
||||||
|
└─ecb-256 x 151,699 ops/sec @ 6μs/op
|
||||||
|
encrypt (8KB)
|
||||||
|
├─ctr-256 x 13,342 ops/sec @ 74μs/op
|
||||||
|
├─cbc-256 x 13,664 ops/sec @ 73μs/op
|
||||||
|
└─ecb-256 x 22,426 ops/sec @ 44μs/op
|
||||||
|
encrypt (1MB)
|
||||||
|
├─ctr-256 x 106 ops/sec @ 9ms/op
|
||||||
|
├─cbc-256 x 109 ops/sec @ 9ms/op
|
||||||
|
└─ecb-256 x 179 ops/sec @ 5ms/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Compare to other implementations:
|
||||||
|
|
||||||
|
```
|
||||||
|
xsalsa20poly1305 (encrypt, 1MB)
|
||||||
|
├─tweetnacl x 108 ops/sec @ 9ms/op
|
||||||
|
└─noble x 190 ops/sec @ 5ms/op
|
||||||
|
|
||||||
|
chacha20poly1305 (encrypt, 1MB)
|
||||||
|
├─node x 1,360 ops/sec @ 735μs/op
|
||||||
|
├─stablelib x 117 ops/sec @ 8ms/op
|
||||||
|
└─noble x 193 ops/sec @ 5ms/op
|
||||||
|
|
||||||
|
chacha (encrypt, 1MB)
|
||||||
|
├─node x 2,035 ops/sec @ 491μs/op
|
||||||
|
├─stablelib x 206 ops/sec @ 4ms/op
|
||||||
|
└─noble x 474 ops/sec @ 2ms/op
|
||||||
|
|
||||||
|
ctr-256 (encrypt, 1MB)
|
||||||
|
├─node x 3,530 ops/sec @ 283μs/op
|
||||||
|
├─stablelib x 70 ops/sec @ 14ms/op
|
||||||
|
├─aesjs x 31 ops/sec @ 32ms/op
|
||||||
|
├─noble-webcrypto x 4,589 ops/sec @ 217μs/op
|
||||||
|
└─noble x 107 ops/sec @ 9ms/op
|
||||||
|
|
||||||
|
cbc-256 (encrypt, 1MB)
|
||||||
|
├─node x 993 ops/sec @ 1ms/op
|
||||||
|
├─stablelib x 63 ops/sec @ 15ms/op
|
||||||
|
├─aesjs x 29 ops/sec @ 34ms/op
|
||||||
|
├─noble-webcrypto x 1,087 ops/sec @ 919μs/op
|
||||||
|
└─noble x 110 ops/sec @ 9ms/op
|
||||||
|
|
||||||
|
gcm-256 (encrypt, 1MB)
|
||||||
|
├─node x 3,196 ops/sec @ 312μs/op
|
||||||
|
├─stablelib x 27 ops/sec @ 36ms/op
|
||||||
|
├─noble-webcrypto x 4,059 ops/sec @ 246μs/op
|
||||||
|
└─noble x 74 ops/sec @ 13ms/op
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
Upgrade from `micro-aes-gcm` package is simple:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// prepare
|
||||||
|
const key = Uint8Array.from([
|
||||||
|
64, 196, 127, 247, 172, 2, 34, 159, 6, 241, 30, 174, 183, 229, 41, 114, 253, 122, 119, 168, 177,
|
||||||
|
243, 155, 236, 164, 159, 98, 72, 162, 243, 224, 195,
|
||||||
|
]);
|
||||||
|
const message = 'Hello world';
|
||||||
|
|
||||||
|
// previous
|
||||||
|
import * as aes from 'micro-aes-gcm';
|
||||||
|
const ciphertext = await aes.encrypt(key, aes.utils.utf8ToBytes(message));
|
||||||
|
const plaintext = await aes.decrypt(key, ciphertext);
|
||||||
|
console.log(aes.utils.bytesToUtf8(plaintext) === message);
|
||||||
|
|
||||||
|
// became =>
|
||||||
|
|
||||||
|
import { gcm } from '@noble/ciphers/aes';
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils';
|
||||||
|
import { managedNonce } from '@noble/ciphers/webcrypto';
|
||||||
|
const aes = managedNonce(gcm)(key);
|
||||||
|
const ciphertext = aes.encrypt(utf8ToBytes(message));
|
||||||
|
const plaintext = aes.decrypt(key, ciphertext);
|
||||||
|
console.log(bytesToUtf8(plaintext) === message);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing & testing
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. `npm install` to install build dependencies like TypeScript
|
||||||
|
3. `npm run build` to compile TypeScript code
|
||||||
|
4. `npm run test` will execute all main tests
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out [paulmillr.com/noble](https://paulmillr.com/noble/)
|
||||||
|
for useful resources, articles, documentation and demos
|
||||||
|
related to the library.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2023 Paul Miller [(https://paulmillr.com)](https://paulmillr.com)
|
||||||
|
Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
||||||
|
|
||||||
|
See LICENSE file.
|
||||||
14
node_modules/@noble/ciphers/_arx.d.ts
generated
vendored
Normal file
14
node_modules/@noble/ciphers/_arx.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { XorStream } from './utils.js';
|
||||||
|
export declare const sigma: Uint32Array;
|
||||||
|
export declare function rotl(a: number, b: number): number;
|
||||||
|
export type CipherCoreFn = (sigma: Uint32Array, key: Uint32Array, nonce: Uint32Array, output: Uint32Array, counter: number, rounds?: number) => void;
|
||||||
|
export type ExtendNonceFn = (sigma: Uint32Array, key: Uint32Array, input: Uint32Array, output: Uint32Array) => void;
|
||||||
|
export type CipherOpts = {
|
||||||
|
allowShortKeys?: boolean;
|
||||||
|
extendNonceFn?: ExtendNonceFn;
|
||||||
|
counterLength?: number;
|
||||||
|
counterRight?: boolean;
|
||||||
|
rounds?: number;
|
||||||
|
};
|
||||||
|
export declare function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream;
|
||||||
|
//# sourceMappingURL=_arx.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/_arx.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_arx.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_arx.d.ts","sourceRoot":"","sources":["src/_arx.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AA4CvD,eAAO,MAAM,KAAK,aAAqB,CAAC;AAExC,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,MAAM,MAAM,YAAY,GAAG,CACzB,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,KAChB,IAAI,CAAC;AAEV,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAwDF,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,GAAG,SAAS,CAsF5E"}
|
||||||
175
node_modules/@noble/ciphers/_arx.js
generated
vendored
Normal file
175
node_modules/@noble/ciphers/_arx.js
generated
vendored
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createCipher = exports.rotl = exports.sigma = void 0;
|
||||||
|
// Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers.
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
/*
|
||||||
|
RFC8439 requires multi-step cipher stream, where
|
||||||
|
authKey starts with counter: 0, actual msg with counter: 1.
|
||||||
|
|
||||||
|
For this, we need a way to re-use nonce / counter:
|
||||||
|
|
||||||
|
const counter = new Uint8Array(4);
|
||||||
|
chacha(..., counter, ...); // counter is now 1
|
||||||
|
chacha(..., counter, ...); // counter is now 2
|
||||||
|
|
||||||
|
This is complicated:
|
||||||
|
|
||||||
|
- 32-bit counters are enough, no need for 64-bit: max ArrayBuffer size in JS is 4GB
|
||||||
|
- Original papers don't allow mutating counters
|
||||||
|
- Counter overflow is undefined [^1]
|
||||||
|
- Idea A: allow providing (nonce | counter) instead of just nonce, re-use it
|
||||||
|
- Caveat: Cannot be re-used through all cases:
|
||||||
|
- * chacha has (counter | nonce)
|
||||||
|
- * xchacha has (nonce16 | counter | nonce16)
|
||||||
|
- Idea B: separate nonce / counter and provide separate API for counter re-use
|
||||||
|
- Caveat: there are different counter sizes depending on an algorithm.
|
||||||
|
- salsa & chacha also differ in structures of key & sigma:
|
||||||
|
salsa20: s[0] | k(4) | s[1] | nonce(2) | ctr(2) | s[2] | k(4) | s[3]
|
||||||
|
chacha: s(4) | k(8) | ctr(1) | nonce(3)
|
||||||
|
chacha20orig: s(4) | k(8) | ctr(2) | nonce(2)
|
||||||
|
- Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)`
|
||||||
|
- Caveat: we can't re-use counter array
|
||||||
|
|
||||||
|
xchacha [^2] uses the subkey and remaining 8 byte nonce with ChaCha20 as normal
|
||||||
|
(prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).
|
||||||
|
|
||||||
|
[^1]: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/
|
||||||
|
[^2]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2
|
||||||
|
*/
|
||||||
|
// We can't make top-level var depend on utils.utf8ToBytes
|
||||||
|
// because it's not present in all envs. Creating a similar fn here
|
||||||
|
const _utf8ToBytes = (str) => Uint8Array.from(str.split('').map((c) => c.charCodeAt(0)));
|
||||||
|
const sigma16 = _utf8ToBytes('expand 16-byte k');
|
||||||
|
const sigma32 = _utf8ToBytes('expand 32-byte k');
|
||||||
|
const sigma16_32 = (0, utils_js_1.u32)(sigma16);
|
||||||
|
const sigma32_32 = (0, utils_js_1.u32)(sigma32);
|
||||||
|
exports.sigma = sigma32_32.slice();
|
||||||
|
function rotl(a, b) {
|
||||||
|
return (a << b) | (a >>> (32 - b));
|
||||||
|
}
|
||||||
|
exports.rotl = rotl;
|
||||||
|
// Is byte array aligned to 4 byte offset (u32)?
|
||||||
|
function isAligned32(b) {
|
||||||
|
return b.byteOffset % 4 === 0;
|
||||||
|
}
|
||||||
|
// Salsa and Chacha block length is always 512-bit
|
||||||
|
const BLOCK_LEN = 64;
|
||||||
|
const BLOCK_LEN32 = 16;
|
||||||
|
// new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ]
|
||||||
|
// new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ]
|
||||||
|
const MAX_COUNTER = 2 ** 32 - 1;
|
||||||
|
const U32_EMPTY = new Uint32Array();
|
||||||
|
function runCipher(core, sigma, key, nonce, data, output, counter, rounds) {
|
||||||
|
const len = data.length;
|
||||||
|
const block = new Uint8Array(BLOCK_LEN);
|
||||||
|
const b32 = (0, utils_js_1.u32)(block);
|
||||||
|
// Make sure that buffers aligned to 4 bytes
|
||||||
|
const isAligned = isAligned32(data) && isAligned32(output);
|
||||||
|
const d32 = isAligned ? (0, utils_js_1.u32)(data) : U32_EMPTY;
|
||||||
|
const o32 = isAligned ? (0, utils_js_1.u32)(output) : U32_EMPTY;
|
||||||
|
for (let pos = 0; pos < len; counter++) {
|
||||||
|
core(sigma, key, nonce, b32, counter, rounds);
|
||||||
|
if (counter >= MAX_COUNTER)
|
||||||
|
throw new Error('arx: counter overflow');
|
||||||
|
const take = Math.min(BLOCK_LEN, len - pos);
|
||||||
|
// aligned to 4 bytes
|
||||||
|
if (isAligned && take === BLOCK_LEN) {
|
||||||
|
const pos32 = pos / 4;
|
||||||
|
if (pos % 4 !== 0)
|
||||||
|
throw new Error('arx: invalid block position');
|
||||||
|
for (let j = 0, posj; j < BLOCK_LEN32; j++) {
|
||||||
|
posj = pos32 + j;
|
||||||
|
o32[posj] = d32[posj] ^ b32[j];
|
||||||
|
}
|
||||||
|
pos += BLOCK_LEN;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let j = 0, posj; j < take; j++) {
|
||||||
|
posj = pos + j;
|
||||||
|
output[posj] = data[posj] ^ block[j];
|
||||||
|
}
|
||||||
|
pos += take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createCipher(core, opts) {
|
||||||
|
const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = (0, utils_js_1.checkOpts)({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts);
|
||||||
|
if (typeof core !== 'function')
|
||||||
|
throw new Error('core must be a function');
|
||||||
|
(0, _assert_js_1.number)(counterLength);
|
||||||
|
(0, _assert_js_1.number)(rounds);
|
||||||
|
(0, _assert_js_1.bool)(counterRight);
|
||||||
|
(0, _assert_js_1.bool)(allowShortKeys);
|
||||||
|
return (key, nonce, data, output, counter = 0) => {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
(0, _assert_js_1.bytes)(data);
|
||||||
|
const len = data.length;
|
||||||
|
if (!output)
|
||||||
|
output = new Uint8Array(len);
|
||||||
|
(0, _assert_js_1.bytes)(output);
|
||||||
|
(0, _assert_js_1.number)(counter);
|
||||||
|
if (counter < 0 || counter >= MAX_COUNTER)
|
||||||
|
throw new Error('arx: counter overflow');
|
||||||
|
if (output.length < len)
|
||||||
|
throw new Error(`arx: output (${output.length}) is shorter than data (${len})`);
|
||||||
|
const toClean = [];
|
||||||
|
// Key & sigma
|
||||||
|
// key=16 -> sigma16, k=key|key
|
||||||
|
// key=32 -> sigma32, k=key
|
||||||
|
let l = key.length, k, sigma;
|
||||||
|
if (l === 32) {
|
||||||
|
k = key.slice();
|
||||||
|
toClean.push(k);
|
||||||
|
sigma = sigma32_32;
|
||||||
|
}
|
||||||
|
else if (l === 16 && allowShortKeys) {
|
||||||
|
k = new Uint8Array(32);
|
||||||
|
k.set(key);
|
||||||
|
k.set(key, 16);
|
||||||
|
sigma = sigma16_32;
|
||||||
|
toClean.push(k);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`arx: invalid 32-byte key, got length=${l}`);
|
||||||
|
}
|
||||||
|
// Nonce
|
||||||
|
// salsa20: 8 (8-byte counter)
|
||||||
|
// chacha20orig: 8 (8-byte counter)
|
||||||
|
// chacha20: 12 (4-byte counter)
|
||||||
|
// xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce)
|
||||||
|
// xchacha20: 24 (16 -> hchacha, 8 -> old nonce)
|
||||||
|
// Align nonce to 4 bytes
|
||||||
|
if (!isAligned32(nonce)) {
|
||||||
|
nonce = nonce.slice();
|
||||||
|
toClean.push(nonce);
|
||||||
|
}
|
||||||
|
const k32 = (0, utils_js_1.u32)(k);
|
||||||
|
// hsalsa & hchacha: handle extended nonce
|
||||||
|
if (extendNonceFn) {
|
||||||
|
if (nonce.length !== 24)
|
||||||
|
throw new Error(`arx: extended nonce must be 24 bytes`);
|
||||||
|
extendNonceFn(sigma, k32, (0, utils_js_1.u32)(nonce.subarray(0, 16)), k32);
|
||||||
|
nonce = nonce.subarray(16);
|
||||||
|
}
|
||||||
|
// Handle nonce counter
|
||||||
|
const nonceNcLen = 16 - counterLength;
|
||||||
|
if (nonceNcLen !== nonce.length)
|
||||||
|
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
|
||||||
|
// Pad counter when nonce is 64 bit
|
||||||
|
if (nonceNcLen !== 12) {
|
||||||
|
const nc = new Uint8Array(12);
|
||||||
|
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
|
||||||
|
nonce = nc;
|
||||||
|
toClean.push(nonce);
|
||||||
|
}
|
||||||
|
const n32 = (0, utils_js_1.u32)(nonce);
|
||||||
|
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
|
||||||
|
while (toClean.length > 0)
|
||||||
|
toClean.pop().fill(0);
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
exports.createCipher = createCipher;
|
||||||
|
//# sourceMappingURL=_arx.js.map
|
||||||
1
node_modules/@noble/ciphers/_arx.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_arx.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
24
node_modules/@noble/ciphers/_assert.d.ts
generated
vendored
Normal file
24
node_modules/@noble/ciphers/_assert.d.ts
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare function number(n: number): void;
|
||||||
|
declare function bool(b: boolean): void;
|
||||||
|
export declare function isBytes(a: unknown): a is Uint8Array;
|
||||||
|
declare function bytes(b: Uint8Array | undefined, ...lengths: number[]): void;
|
||||||
|
export type Hash = {
|
||||||
|
(data: Uint8Array): Uint8Array;
|
||||||
|
blockLen: number;
|
||||||
|
outputLen: number;
|
||||||
|
create: any;
|
||||||
|
};
|
||||||
|
declare function hash(hash: Hash): void;
|
||||||
|
declare function exists(instance: any, checkFinished?: boolean): void;
|
||||||
|
declare function output(out: any, instance: any): void;
|
||||||
|
export { number, bool, bytes, hash, exists, output };
|
||||||
|
declare const assert: {
|
||||||
|
number: typeof number;
|
||||||
|
bool: typeof bool;
|
||||||
|
bytes: typeof bytes;
|
||||||
|
hash: typeof hash;
|
||||||
|
exists: typeof exists;
|
||||||
|
output: typeof output;
|
||||||
|
};
|
||||||
|
export default assert;
|
||||||
|
//# sourceMappingURL=_assert.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/_assert.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_assert.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_assert.d.ts","sourceRoot":"","sources":["src/_assert.ts"],"names":[],"mappings":"AAAA,iBAAS,MAAM,CAAC,CAAC,EAAE,MAAM,QAExB;AAED,iBAAS,IAAI,CAAC,CAAC,EAAE,OAAO,QAEvB;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAKnD;AAED,iBAAS,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,QAI7D;AAED,MAAM,MAAM,IAAI,GAAG;IACjB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;CACb,CAAC;AACF,iBAAS,IAAI,CAAC,IAAI,EAAE,IAAI,QAKvB;AAED,iBAAS,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,aAAa,UAAO,QAGlD;AAED,iBAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,QAMtC;AAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACrD,QAAA,MAAM,MAAM;;;;;;;CAAgD,CAAC;AAC7D,eAAe,MAAM,CAAC"}
|
||||||
50
node_modules/@noble/ciphers/_assert.js
generated
vendored
Normal file
50
node_modules/@noble/ciphers/_assert.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.output = exports.exists = exports.hash = exports.bytes = exports.bool = exports.number = exports.isBytes = void 0;
|
||||||
|
function number(n) {
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0)
|
||||||
|
throw new Error(`positive integer expected, not ${n}`);
|
||||||
|
}
|
||||||
|
exports.number = number;
|
||||||
|
function bool(b) {
|
||||||
|
if (typeof b !== 'boolean')
|
||||||
|
throw new Error(`boolean expected, not ${b}`);
|
||||||
|
}
|
||||||
|
exports.bool = bool;
|
||||||
|
function isBytes(a) {
|
||||||
|
return (a instanceof Uint8Array ||
|
||||||
|
(a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array'));
|
||||||
|
}
|
||||||
|
exports.isBytes = isBytes;
|
||||||
|
function bytes(b, ...lengths) {
|
||||||
|
if (!isBytes(b))
|
||||||
|
throw new Error('Uint8Array expected');
|
||||||
|
if (lengths.length > 0 && !lengths.includes(b.length))
|
||||||
|
throw new Error(`Uint8Array expected of length ${lengths}, not of length=${b.length}`);
|
||||||
|
}
|
||||||
|
exports.bytes = bytes;
|
||||||
|
function hash(hash) {
|
||||||
|
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
||||||
|
throw new Error('hash must be wrapped by utils.wrapConstructor');
|
||||||
|
number(hash.outputLen);
|
||||||
|
number(hash.blockLen);
|
||||||
|
}
|
||||||
|
exports.hash = hash;
|
||||||
|
function exists(instance, checkFinished = true) {
|
||||||
|
if (instance.destroyed)
|
||||||
|
throw new Error('Hash instance has been destroyed');
|
||||||
|
if (checkFinished && instance.finished)
|
||||||
|
throw new Error('Hash#digest() has already been called');
|
||||||
|
}
|
||||||
|
exports.exists = exists;
|
||||||
|
function output(out, instance) {
|
||||||
|
bytes(out);
|
||||||
|
const min = instance.outputLen;
|
||||||
|
if (out.length < min) {
|
||||||
|
throw new Error(`digestInto() expects output buffer of length at least ${min}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.output = output;
|
||||||
|
const assert = { number, bool, bytes, hash, exists, output };
|
||||||
|
exports.default = assert;
|
||||||
|
//# sourceMappingURL=_assert.js.map
|
||||||
1
node_modules/@noble/ciphers/_assert.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_assert.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_assert.js","sourceRoot":"","sources":["src/_assert.ts"],"names":[],"mappings":";;;AAAA,SAAS,MAAM,CAAC,CAAS;IACvB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,EAAE,CAAC,CAAC;AAChG,CAAC;AA6CQ,wBAAM;AA3Cf,SAAS,IAAI,CAAC,CAAU;IACtB,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAyCgB,oBAAI;AAvCrB,SAAgB,OAAO,CAAC,CAAU;IAChC,OAAO,CACL,CAAC,YAAY,UAAU;QACvB,CAAC,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,KAAK,YAAY,CAAC,CAC5E,CAAC;AACJ,CAAC;AALD,0BAKC;AAED,SAAS,KAAK,CAAC,CAAyB,EAAE,GAAG,OAAiB;IAC5D,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,mBAAmB,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AAC3F,CAAC;AA4BsB,sBAAK;AApB5B,SAAS,IAAI,CAAC,IAAU;IACtB,IAAI,OAAO,IAAI,KAAK,UAAU,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU;QACjE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AAe6B,oBAAI;AAblC,SAAS,MAAM,CAAC,QAAa,EAAE,aAAa,GAAG,IAAI;IACjD,IAAI,QAAQ,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC5E,IAAI,aAAa,IAAI,QAAQ,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;AACnG,CAAC;AAUmC,wBAAM;AAR1C,SAAS,MAAM,CAAC,GAAQ,EAAE,QAAa;IACrC,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC;IAC/B,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,yDAAyD,GAAG,EAAE,CAAC,CAAC;IAClF,CAAC;AACH,CAAC;AAE2C,wBAAM;AAClD,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC7D,kBAAe,MAAM,CAAC"}
|
||||||
70
node_modules/@noble/ciphers/_micro.d.ts
generated
vendored
Normal file
70
node_modules/@noble/ciphers/_micro.d.ts
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||||
|
import { Cipher, XorStream } from './utils.js';
|
||||||
|
export declare function hsalsa(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array): void;
|
||||||
|
export declare function hchacha(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array): void;
|
||||||
|
/**
|
||||||
|
* salsa20, 12-byte nonce.
|
||||||
|
*/
|
||||||
|
export declare const salsa20: XorStream;
|
||||||
|
/**
|
||||||
|
* xsalsa20, 24-byte nonce.
|
||||||
|
*/
|
||||||
|
export declare const xsalsa20: XorStream;
|
||||||
|
/**
|
||||||
|
* chacha20 non-RFC, original version by djb. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
export declare const chacha20orig: XorStream;
|
||||||
|
/**
|
||||||
|
* chacha20 RFC 8439 (IETF / TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
*/
|
||||||
|
export declare const chacha20: XorStream;
|
||||||
|
/**
|
||||||
|
* xchacha20 eXtended-nonce. https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
export declare const xchacha20: XorStream;
|
||||||
|
/**
|
||||||
|
* 8-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
export declare const chacha8: XorStream;
|
||||||
|
/**
|
||||||
|
* 12-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
export declare const chacha12: XorStream;
|
||||||
|
export declare function poly1305(msg: Uint8Array, key: Uint8Array): Uint8Array;
|
||||||
|
/**
|
||||||
|
* xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa.
|
||||||
|
*/
|
||||||
|
export declare const xsalsa20poly1305: ((key: Uint8Array, nonce: Uint8Array) => {
|
||||||
|
encrypt: (plaintext: Uint8Array) => Uint8Array;
|
||||||
|
decrypt: (ciphertext: Uint8Array) => Uint8Array;
|
||||||
|
}) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Alias to xsalsa20-poly1305
|
||||||
|
*/
|
||||||
|
export declare function secretbox(key: Uint8Array, nonce: Uint8Array): {
|
||||||
|
seal: (plaintext: Uint8Array) => Uint8Array;
|
||||||
|
open: (ciphertext: Uint8Array) => Uint8Array;
|
||||||
|
};
|
||||||
|
export declare const _poly1305_aead: (fn: XorStream) => (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher;
|
||||||
|
/**
|
||||||
|
* chacha20-poly1305 12-byte-nonce chacha.
|
||||||
|
*/
|
||||||
|
export declare const chacha20poly1305: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* xchacha20-poly1305 eXtended-nonce (24 bytes) chacha.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
export declare const xchacha20poly1305: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=_micro.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/_micro.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_micro.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_micro.d.ts","sourceRoot":"","sources":["src/_micro.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EACL,MAAM,EAAE,SAAS,EAElB,MAAM,YAAY,CAAC;AA+EpB,wBAAgB,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,QAatF;AAuBD,wBAAgB,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,QAavF;AAED;;GAEG;AACH,eAAO,MAAM,OAAO,WAGlB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ,WAGnB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY,WAIvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ,WAGnB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,WAIpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,OAAO,WAIlB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ,WAInB,CAAC;AAQH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,GAAG,UAAU,CAcrE;AA4BD;;GAEG;AACH,eAAO,MAAM,gBAAgB,SAEI,UAAU,SAAS,UAAU;yBAInC,UAAU;0BAST,UAAU;;;;;CAWrC,CAAC;AAEF;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;;;EAG3D;AAED,eAAO,MAAM,cAAc,OACpB,SAAS,WACR,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,MAuBvD,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,gBAAgB,SA5BrB,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,MAAM;;;;CA+B/D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,SArCtB,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,MAAM;;;;CAwC/D,CAAC"}
|
||||||
295
node_modules/@noble/ciphers/_micro.js
generated
vendored
Normal file
295
node_modules/@noble/ciphers/_micro.js
generated
vendored
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.xchacha20poly1305 = exports.chacha20poly1305 = exports._poly1305_aead = exports.secretbox = exports.xsalsa20poly1305 = exports.poly1305 = exports.chacha12 = exports.chacha8 = exports.xchacha20 = exports.chacha20 = exports.chacha20orig = exports.xsalsa20 = exports.salsa20 = exports.hchacha = exports.hsalsa = void 0;
|
||||||
|
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||||
|
// prettier-ignore
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
const _arx_js_1 = require("./_arx.js");
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
/*
|
||||||
|
noble-ciphers-micro: more auditable, but slower version of salsa20, chacha & poly1305.
|
||||||
|
Implements the same algorithms that are present in other files, but without
|
||||||
|
unrolled loops (https://en.wikipedia.org/wiki/Loop_unrolling).
|
||||||
|
*/
|
||||||
|
function bytesToNumberLE(bytes) {
|
||||||
|
return (0, utils_js_1.hexToNumber)((0, utils_js_1.bytesToHex)(Uint8Array.from(bytes).reverse()));
|
||||||
|
}
|
||||||
|
function numberToBytesLE(n, len) {
|
||||||
|
return (0, utils_js_1.numberToBytesBE)(n, len).reverse();
|
||||||
|
}
|
||||||
|
function salsaQR(x, a, b, c, d) {
|
||||||
|
x[b] ^= (0, _arx_js_1.rotl)((x[a] + x[d]) | 0, 7);
|
||||||
|
x[c] ^= (0, _arx_js_1.rotl)((x[b] + x[a]) | 0, 9);
|
||||||
|
x[d] ^= (0, _arx_js_1.rotl)((x[c] + x[b]) | 0, 13);
|
||||||
|
x[a] ^= (0, _arx_js_1.rotl)((x[d] + x[c]) | 0, 18);
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
function chachaQR(x, a, b, c, d) {
|
||||||
|
x[a] = (x[a] + x[b]) | 0;
|
||||||
|
x[d] = (0, _arx_js_1.rotl)(x[d] ^ x[a], 16);
|
||||||
|
x[c] = (x[c] + x[d]) | 0;
|
||||||
|
x[b] = (0, _arx_js_1.rotl)(x[b] ^ x[c], 12);
|
||||||
|
x[a] = (x[a] + x[b]) | 0;
|
||||||
|
x[d] = (0, _arx_js_1.rotl)(x[d] ^ x[a], 8);
|
||||||
|
x[c] = (x[c] + x[d]) | 0;
|
||||||
|
x[b] = (0, _arx_js_1.rotl)(x[b] ^ x[c], 7);
|
||||||
|
}
|
||||||
|
function salsaRound(x, rounds = 20) {
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
salsaQR(x, 0, 4, 8, 12);
|
||||||
|
salsaQR(x, 5, 9, 13, 1);
|
||||||
|
salsaQR(x, 10, 14, 2, 6);
|
||||||
|
salsaQR(x, 15, 3, 7, 11);
|
||||||
|
salsaQR(x, 0, 1, 2, 3);
|
||||||
|
salsaQR(x, 5, 6, 7, 4);
|
||||||
|
salsaQR(x, 10, 11, 8, 9);
|
||||||
|
salsaQR(x, 15, 12, 13, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function chachaRound(x, rounds = 20) {
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
chachaQR(x, 0, 4, 8, 12);
|
||||||
|
chachaQR(x, 1, 5, 9, 13);
|
||||||
|
chachaQR(x, 2, 6, 10, 14);
|
||||||
|
chachaQR(x, 3, 7, 11, 15);
|
||||||
|
chachaQR(x, 0, 5, 10, 15);
|
||||||
|
chachaQR(x, 1, 6, 11, 12);
|
||||||
|
chachaQR(x, 2, 7, 8, 13);
|
||||||
|
chachaQR(x, 3, 4, 9, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function salsaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
// prettier-ignore
|
||||||
|
const y = new Uint32Array([
|
||||||
|
s[0], k[0], k[1], k[2], // "expa" Key Key Key
|
||||||
|
k[3], s[1], n[0], n[1], // Key "nd 3" Nonce Nonce
|
||||||
|
cnt, 0, s[2], k[4], // Pos. Pos. "2-by" Key
|
||||||
|
k[5], k[6], k[7], s[3], // Key Key Key "te k"
|
||||||
|
]);
|
||||||
|
const x = y.slice();
|
||||||
|
salsaRound(x, rounds);
|
||||||
|
for (let i = 0; i < 16; i++)
|
||||||
|
out[i] = (y[i] + x[i]) | 0;
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
function hsalsa(s, k, i, o32) {
|
||||||
|
const x = new Uint32Array([
|
||||||
|
s[0], k[0], k[1], k[2],
|
||||||
|
k[3], s[1], i[0], i[1],
|
||||||
|
i[2], i[3], s[2], k[4],
|
||||||
|
k[5], k[6], k[7], s[3]
|
||||||
|
]);
|
||||||
|
salsaRound(x, 20);
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x[0];
|
||||||
|
o32[oi++] = x[5];
|
||||||
|
o32[oi++] = x[10];
|
||||||
|
o32[oi++] = x[15];
|
||||||
|
o32[oi++] = x[6];
|
||||||
|
o32[oi++] = x[7];
|
||||||
|
o32[oi++] = x[8];
|
||||||
|
o32[oi++] = x[9];
|
||||||
|
}
|
||||||
|
exports.hsalsa = hsalsa;
|
||||||
|
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
// prettier-ignore
|
||||||
|
const y = new Uint32Array([
|
||||||
|
s[0], s[1], s[2], s[3], // "expa" "nd 3" "2-by" "te k"
|
||||||
|
k[0], k[1], k[2], k[3], // Key Key Key Key
|
||||||
|
k[4], k[5], k[6], k[7], // Key Key Key Key
|
||||||
|
cnt, n[0], n[1], n[2], // Counter Counter Nonce Nonce
|
||||||
|
]);
|
||||||
|
const x = y.slice();
|
||||||
|
chachaRound(x, rounds);
|
||||||
|
for (let i = 0; i < 16; i++)
|
||||||
|
out[i] = (y[i] + x[i]) | 0;
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
function hchacha(s, k, i, o32) {
|
||||||
|
const x = new Uint32Array([
|
||||||
|
s[0], s[1], s[2], s[3],
|
||||||
|
k[0], k[1], k[2], k[3],
|
||||||
|
k[4], k[5], k[6], k[7],
|
||||||
|
i[0], i[1], i[2], i[3],
|
||||||
|
]);
|
||||||
|
chachaRound(x, 20);
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x[0];
|
||||||
|
o32[oi++] = x[1];
|
||||||
|
o32[oi++] = x[2];
|
||||||
|
o32[oi++] = x[3];
|
||||||
|
o32[oi++] = x[12];
|
||||||
|
o32[oi++] = x[13];
|
||||||
|
o32[oi++] = x[14];
|
||||||
|
o32[oi++] = x[15];
|
||||||
|
}
|
||||||
|
exports.hchacha = hchacha;
|
||||||
|
/**
|
||||||
|
* salsa20, 12-byte nonce.
|
||||||
|
*/
|
||||||
|
exports.salsa20 = (0, _arx_js_1.createCipher)(salsaCore, {
|
||||||
|
allowShortKeys: true,
|
||||||
|
counterRight: true,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* xsalsa20, 24-byte nonce.
|
||||||
|
*/
|
||||||
|
exports.xsalsa20 = (0, _arx_js_1.createCipher)(salsaCore, {
|
||||||
|
counterRight: true,
|
||||||
|
extendNonceFn: hsalsa,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* chacha20 non-RFC, original version by djb. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
exports.chacha20orig = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
allowShortKeys: true,
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* chacha20 RFC 8439 (IETF / TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
*/
|
||||||
|
exports.chacha20 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* xchacha20 eXtended-nonce. https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
exports.xchacha20 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
extendNonceFn: hchacha,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 8-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
exports.chacha8 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 12-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
exports.chacha12 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 12,
|
||||||
|
});
|
||||||
|
const POW_2_130_5 = BigInt(2) ** BigInt(130) - BigInt(5);
|
||||||
|
const POW_2_128_1 = BigInt(2) ** BigInt(16 * 8) - BigInt(1);
|
||||||
|
const CLAMP_R = BigInt('0x0ffffffc0ffffffc0ffffffc0fffffff');
|
||||||
|
const _0 = BigInt(0);
|
||||||
|
const _1 = BigInt(1);
|
||||||
|
// Can be speed-up using BigUint64Array, but would be more complicated
|
||||||
|
function poly1305(msg, key) {
|
||||||
|
(0, _assert_js_1.bytes)(msg);
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
let acc = _0;
|
||||||
|
const r = bytesToNumberLE(key.subarray(0, 16)) & CLAMP_R;
|
||||||
|
const s = bytesToNumberLE(key.subarray(16));
|
||||||
|
// Process by 16 byte chunks
|
||||||
|
for (let i = 0; i < msg.length; i += 16) {
|
||||||
|
const m = msg.subarray(i, i + 16);
|
||||||
|
const n = bytesToNumberLE(m) | (_1 << BigInt(8 * m.length));
|
||||||
|
acc = ((acc + n) * r) % POW_2_130_5;
|
||||||
|
}
|
||||||
|
const res = (acc + s) & POW_2_128_1;
|
||||||
|
return numberToBytesLE(res, 16);
|
||||||
|
}
|
||||||
|
exports.poly1305 = poly1305;
|
||||||
|
function computeTag(fn, key, nonce, ciphertext, AAD) {
|
||||||
|
const res = [];
|
||||||
|
if (AAD) {
|
||||||
|
res.push(AAD);
|
||||||
|
const leftover = AAD.length % 16;
|
||||||
|
if (leftover > 0)
|
||||||
|
res.push(new Uint8Array(16 - leftover));
|
||||||
|
}
|
||||||
|
res.push(ciphertext);
|
||||||
|
const leftover = ciphertext.length % 16;
|
||||||
|
if (leftover > 0)
|
||||||
|
res.push(new Uint8Array(16 - leftover));
|
||||||
|
// Lengths
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = (0, utils_js_1.createView)(num);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 8, BigInt(ciphertext.length), true);
|
||||||
|
res.push(num);
|
||||||
|
const authKey = fn(key, nonce, new Uint8Array(32));
|
||||||
|
return poly1305((0, utils_js_1.concatBytes)(...res), authKey);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa.
|
||||||
|
*/
|
||||||
|
exports.xsalsa20poly1305 = (0, utils_js_1.wrapCipher)({ blockSize: 64, nonceLength: 24, tagLength: 16 }, function xsalsa20poly1305(key, nonce) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
(0, _assert_js_1.bytes)(plaintext);
|
||||||
|
const m = (0, utils_js_1.concatBytes)(new Uint8Array(32), plaintext);
|
||||||
|
const c = (0, exports.xsalsa20)(key, nonce, m);
|
||||||
|
const authKey = c.subarray(0, 32);
|
||||||
|
const data = c.subarray(32);
|
||||||
|
const tag = poly1305(data, authKey);
|
||||||
|
return (0, utils_js_1.concatBytes)(tag, data);
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
(0, _assert_js_1.bytes)(ciphertext);
|
||||||
|
if (ciphertext.length < 16)
|
||||||
|
throw new Error('encrypted data must be at least 16 bytes');
|
||||||
|
const c = (0, utils_js_1.concatBytes)(new Uint8Array(16), ciphertext);
|
||||||
|
const authKey = (0, exports.xsalsa20)(key, nonce, new Uint8Array(32));
|
||||||
|
const tag = poly1305(c.subarray(32), authKey);
|
||||||
|
if (!(0, utils_js_1.equalBytes)(c.subarray(16, 32), tag))
|
||||||
|
throw new Error('invalid poly1305 tag');
|
||||||
|
return (0, exports.xsalsa20)(key, nonce, c).subarray(32);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Alias to xsalsa20-poly1305
|
||||||
|
*/
|
||||||
|
function secretbox(key, nonce) {
|
||||||
|
const xs = (0, exports.xsalsa20poly1305)(key, nonce);
|
||||||
|
return { seal: xs.encrypt, open: xs.decrypt };
|
||||||
|
}
|
||||||
|
exports.secretbox = secretbox;
|
||||||
|
const _poly1305_aead = (fn) => (key, nonce, AAD) => {
|
||||||
|
const tagLength = 16;
|
||||||
|
const keyLength = 32;
|
||||||
|
(0, _assert_js_1.bytes)(key, keyLength);
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
(0, _assert_js_1.bytes)(plaintext);
|
||||||
|
const res = fn(key, nonce, plaintext, undefined, 1);
|
||||||
|
const tag = computeTag(fn, key, nonce, res, AAD);
|
||||||
|
return (0, utils_js_1.concatBytes)(res, tag);
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
(0, _assert_js_1.bytes)(ciphertext);
|
||||||
|
if (ciphertext.length < tagLength)
|
||||||
|
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const tag = computeTag(fn, key, nonce, data, AAD);
|
||||||
|
if (!(0, utils_js_1.equalBytes)(passedTag, tag))
|
||||||
|
throw new Error('invalid poly1305 tag');
|
||||||
|
return fn(key, nonce, data, undefined, 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
exports._poly1305_aead = _poly1305_aead;
|
||||||
|
/**
|
||||||
|
* chacha20-poly1305 12-byte-nonce chacha.
|
||||||
|
*/
|
||||||
|
exports.chacha20poly1305 = (0, utils_js_1.wrapCipher)({ blockSize: 64, nonceLength: 12, tagLength: 16 }, (0, exports._poly1305_aead)(exports.chacha20));
|
||||||
|
/**
|
||||||
|
* xchacha20-poly1305 eXtended-nonce (24 bytes) chacha.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
exports.xchacha20poly1305 = (0, utils_js_1.wrapCipher)({ blockSize: 64, nonceLength: 24, tagLength: 16 }, (0, exports._poly1305_aead)(exports.xchacha20));
|
||||||
|
//# sourceMappingURL=_micro.js.map
|
||||||
1
node_modules/@noble/ciphers/_micro.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_micro.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
15
node_modules/@noble/ciphers/_poly1305.d.ts
generated
vendored
Normal file
15
node_modules/@noble/ciphers/_poly1305.d.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Input, Hash } from './utils.js';
|
||||||
|
export type CHash = ReturnType<typeof wrapConstructorWithKey>;
|
||||||
|
export declare function wrapConstructorWithKey<H extends Hash<H>>(hashCons: (key: Input) => Hash<H>): {
|
||||||
|
(msg: Input, key: Input): Uint8Array;
|
||||||
|
outputLen: number;
|
||||||
|
blockLen: number;
|
||||||
|
create(key: Input): Hash<H>;
|
||||||
|
};
|
||||||
|
export declare const poly1305: {
|
||||||
|
(msg: Input, key: Input): Uint8Array;
|
||||||
|
outputLen: number;
|
||||||
|
blockLen: number;
|
||||||
|
create(key: Input): Hash<Hash<unknown>>;
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=_poly1305.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/_poly1305.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_poly1305.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_poly1305.d.ts","sourceRoot":"","sources":["src/_poly1305.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAW,IAAI,EAAE,MAAM,YAAY,CAAC;AAkRlD,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC9D,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC;UACrE,KAAK,OAAO,KAAK,GAAG,UAAU;;;gBAI7B,KAAK;EAE3B;AAED,eAAO,MAAM,QAAQ;UARC,KAAK,OAAO,KAAK,GAAG,UAAU;;;gBAI7B,KAAK;CAI8C,CAAC"}
|
||||||
268
node_modules/@noble/ciphers/_poly1305.js
generated
vendored
Normal file
268
node_modules/@noble/ciphers/_poly1305.js
generated
vendored
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.poly1305 = exports.wrapConstructorWithKey = void 0;
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
// Poly1305 is a fast and parallel secret-key message-authentication code.
|
||||||
|
// https://cr.yp.to/mac.html, https://cr.yp.to/mac/poly1305-20050329.pdf
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8439
|
||||||
|
// Based on Public Domain poly1305-donna https://github.com/floodyberry/poly1305-donna
|
||||||
|
const u8to16 = (a, i) => (a[i++] & 0xff) | ((a[i++] & 0xff) << 8);
|
||||||
|
class Poly1305 {
|
||||||
|
constructor(key) {
|
||||||
|
this.blockLen = 16;
|
||||||
|
this.outputLen = 16;
|
||||||
|
this.buffer = new Uint8Array(16);
|
||||||
|
this.r = new Uint16Array(10);
|
||||||
|
this.h = new Uint16Array(10);
|
||||||
|
this.pad = new Uint16Array(8);
|
||||||
|
this.pos = 0;
|
||||||
|
this.finished = false;
|
||||||
|
key = (0, utils_js_1.toBytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(key, 32);
|
||||||
|
const t0 = u8to16(key, 0);
|
||||||
|
const t1 = u8to16(key, 2);
|
||||||
|
const t2 = u8to16(key, 4);
|
||||||
|
const t3 = u8to16(key, 6);
|
||||||
|
const t4 = u8to16(key, 8);
|
||||||
|
const t5 = u8to16(key, 10);
|
||||||
|
const t6 = u8to16(key, 12);
|
||||||
|
const t7 = u8to16(key, 14);
|
||||||
|
// https://github.com/floodyberry/poly1305-donna/blob/e6ad6e091d30d7f4ec2d4f978be1fcfcbce72781/poly1305-donna-16.h#L47
|
||||||
|
this.r[0] = t0 & 0x1fff;
|
||||||
|
this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff;
|
||||||
|
this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03;
|
||||||
|
this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff;
|
||||||
|
this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff;
|
||||||
|
this.r[5] = (t4 >>> 1) & 0x1ffe;
|
||||||
|
this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff;
|
||||||
|
this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81;
|
||||||
|
this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff;
|
||||||
|
this.r[9] = (t7 >>> 5) & 0x007f;
|
||||||
|
for (let i = 0; i < 8; i++)
|
||||||
|
this.pad[i] = u8to16(key, 16 + 2 * i);
|
||||||
|
}
|
||||||
|
process(data, offset, isLast = false) {
|
||||||
|
const hibit = isLast ? 0 : 1 << 11;
|
||||||
|
const { h, r } = this;
|
||||||
|
const r0 = r[0];
|
||||||
|
const r1 = r[1];
|
||||||
|
const r2 = r[2];
|
||||||
|
const r3 = r[3];
|
||||||
|
const r4 = r[4];
|
||||||
|
const r5 = r[5];
|
||||||
|
const r6 = r[6];
|
||||||
|
const r7 = r[7];
|
||||||
|
const r8 = r[8];
|
||||||
|
const r9 = r[9];
|
||||||
|
const t0 = u8to16(data, offset + 0);
|
||||||
|
const t1 = u8to16(data, offset + 2);
|
||||||
|
const t2 = u8to16(data, offset + 4);
|
||||||
|
const t3 = u8to16(data, offset + 6);
|
||||||
|
const t4 = u8to16(data, offset + 8);
|
||||||
|
const t5 = u8to16(data, offset + 10);
|
||||||
|
const t6 = u8to16(data, offset + 12);
|
||||||
|
const t7 = u8to16(data, offset + 14);
|
||||||
|
let h0 = h[0] + (t0 & 0x1fff);
|
||||||
|
let h1 = h[1] + (((t0 >>> 13) | (t1 << 3)) & 0x1fff);
|
||||||
|
let h2 = h[2] + (((t1 >>> 10) | (t2 << 6)) & 0x1fff);
|
||||||
|
let h3 = h[3] + (((t2 >>> 7) | (t3 << 9)) & 0x1fff);
|
||||||
|
let h4 = h[4] + (((t3 >>> 4) | (t4 << 12)) & 0x1fff);
|
||||||
|
let h5 = h[5] + ((t4 >>> 1) & 0x1fff);
|
||||||
|
let h6 = h[6] + (((t4 >>> 14) | (t5 << 2)) & 0x1fff);
|
||||||
|
let h7 = h[7] + (((t5 >>> 11) | (t6 << 5)) & 0x1fff);
|
||||||
|
let h8 = h[8] + (((t6 >>> 8) | (t7 << 8)) & 0x1fff);
|
||||||
|
let h9 = h[9] + ((t7 >>> 5) | hibit);
|
||||||
|
let c = 0;
|
||||||
|
let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6);
|
||||||
|
c = d0 >>> 13;
|
||||||
|
d0 &= 0x1fff;
|
||||||
|
d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1);
|
||||||
|
c += d0 >>> 13;
|
||||||
|
d0 &= 0x1fff;
|
||||||
|
let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7);
|
||||||
|
c = d1 >>> 13;
|
||||||
|
d1 &= 0x1fff;
|
||||||
|
d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2);
|
||||||
|
c += d1 >>> 13;
|
||||||
|
d1 &= 0x1fff;
|
||||||
|
let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8);
|
||||||
|
c = d2 >>> 13;
|
||||||
|
d2 &= 0x1fff;
|
||||||
|
d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3);
|
||||||
|
c += d2 >>> 13;
|
||||||
|
d2 &= 0x1fff;
|
||||||
|
let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9);
|
||||||
|
c = d3 >>> 13;
|
||||||
|
d3 &= 0x1fff;
|
||||||
|
d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4);
|
||||||
|
c += d3 >>> 13;
|
||||||
|
d3 &= 0x1fff;
|
||||||
|
let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
|
||||||
|
c = d4 >>> 13;
|
||||||
|
d4 &= 0x1fff;
|
||||||
|
d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5);
|
||||||
|
c += d4 >>> 13;
|
||||||
|
d4 &= 0x1fff;
|
||||||
|
let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1;
|
||||||
|
c = d5 >>> 13;
|
||||||
|
d5 &= 0x1fff;
|
||||||
|
d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6);
|
||||||
|
c += d5 >>> 13;
|
||||||
|
d5 &= 0x1fff;
|
||||||
|
let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2;
|
||||||
|
c = d6 >>> 13;
|
||||||
|
d6 &= 0x1fff;
|
||||||
|
d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7);
|
||||||
|
c += d6 >>> 13;
|
||||||
|
d6 &= 0x1fff;
|
||||||
|
let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3;
|
||||||
|
c = d7 >>> 13;
|
||||||
|
d7 &= 0x1fff;
|
||||||
|
d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8);
|
||||||
|
c += d7 >>> 13;
|
||||||
|
d7 &= 0x1fff;
|
||||||
|
let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4;
|
||||||
|
c = d8 >>> 13;
|
||||||
|
d8 &= 0x1fff;
|
||||||
|
d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9);
|
||||||
|
c += d8 >>> 13;
|
||||||
|
d8 &= 0x1fff;
|
||||||
|
let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5;
|
||||||
|
c = d9 >>> 13;
|
||||||
|
d9 &= 0x1fff;
|
||||||
|
d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0;
|
||||||
|
c += d9 >>> 13;
|
||||||
|
d9 &= 0x1fff;
|
||||||
|
c = ((c << 2) + c) | 0;
|
||||||
|
c = (c + d0) | 0;
|
||||||
|
d0 = c & 0x1fff;
|
||||||
|
c = c >>> 13;
|
||||||
|
d1 += c;
|
||||||
|
h[0] = d0;
|
||||||
|
h[1] = d1;
|
||||||
|
h[2] = d2;
|
||||||
|
h[3] = d3;
|
||||||
|
h[4] = d4;
|
||||||
|
h[5] = d5;
|
||||||
|
h[6] = d6;
|
||||||
|
h[7] = d7;
|
||||||
|
h[8] = d8;
|
||||||
|
h[9] = d9;
|
||||||
|
}
|
||||||
|
finalize() {
|
||||||
|
const { h, pad } = this;
|
||||||
|
const g = new Uint16Array(10);
|
||||||
|
let c = h[1] >>> 13;
|
||||||
|
h[1] &= 0x1fff;
|
||||||
|
for (let i = 2; i < 10; i++) {
|
||||||
|
h[i] += c;
|
||||||
|
c = h[i] >>> 13;
|
||||||
|
h[i] &= 0x1fff;
|
||||||
|
}
|
||||||
|
h[0] += c * 5;
|
||||||
|
c = h[0] >>> 13;
|
||||||
|
h[0] &= 0x1fff;
|
||||||
|
h[1] += c;
|
||||||
|
c = h[1] >>> 13;
|
||||||
|
h[1] &= 0x1fff;
|
||||||
|
h[2] += c;
|
||||||
|
g[0] = h[0] + 5;
|
||||||
|
c = g[0] >>> 13;
|
||||||
|
g[0] &= 0x1fff;
|
||||||
|
for (let i = 1; i < 10; i++) {
|
||||||
|
g[i] = h[i] + c;
|
||||||
|
c = g[i] >>> 13;
|
||||||
|
g[i] &= 0x1fff;
|
||||||
|
}
|
||||||
|
g[9] -= 1 << 13;
|
||||||
|
let mask = (c ^ 1) - 1;
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
g[i] &= mask;
|
||||||
|
mask = ~mask;
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
h[i] = (h[i] & mask) | g[i];
|
||||||
|
h[0] = (h[0] | (h[1] << 13)) & 0xffff;
|
||||||
|
h[1] = ((h[1] >>> 3) | (h[2] << 10)) & 0xffff;
|
||||||
|
h[2] = ((h[2] >>> 6) | (h[3] << 7)) & 0xffff;
|
||||||
|
h[3] = ((h[3] >>> 9) | (h[4] << 4)) & 0xffff;
|
||||||
|
h[4] = ((h[4] >>> 12) | (h[5] << 1) | (h[6] << 14)) & 0xffff;
|
||||||
|
h[5] = ((h[6] >>> 2) | (h[7] << 11)) & 0xffff;
|
||||||
|
h[6] = ((h[7] >>> 5) | (h[8] << 8)) & 0xffff;
|
||||||
|
h[7] = ((h[8] >>> 8) | (h[9] << 5)) & 0xffff;
|
||||||
|
let f = h[0] + pad[0];
|
||||||
|
h[0] = f & 0xffff;
|
||||||
|
for (let i = 1; i < 8; i++) {
|
||||||
|
f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0;
|
||||||
|
h[i] = f & 0xffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
const { buffer, blockLen } = this;
|
||||||
|
data = (0, utils_js_1.toBytes)(data);
|
||||||
|
const len = data.length;
|
||||||
|
for (let pos = 0; pos < len;) {
|
||||||
|
const take = Math.min(blockLen - this.pos, len - pos);
|
||||||
|
// Fast path: we have at least one block in input
|
||||||
|
if (take === blockLen) {
|
||||||
|
for (; blockLen <= len - pos; pos += blockLen)
|
||||||
|
this.process(data, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer.set(data.subarray(pos, pos + take), this.pos);
|
||||||
|
this.pos += take;
|
||||||
|
pos += take;
|
||||||
|
if (this.pos === blockLen) {
|
||||||
|
this.process(buffer, 0, false);
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this.h.fill(0);
|
||||||
|
this.r.fill(0);
|
||||||
|
this.buffer.fill(0);
|
||||||
|
this.pad.fill(0);
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
(0, _assert_js_1.output)(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
const { buffer, h } = this;
|
||||||
|
let { pos } = this;
|
||||||
|
if (pos) {
|
||||||
|
buffer[pos++] = 1;
|
||||||
|
// buffer.subarray(pos).fill(0);
|
||||||
|
for (; pos < 16; pos++)
|
||||||
|
buffer[pos] = 0;
|
||||||
|
this.process(buffer, 0, true);
|
||||||
|
}
|
||||||
|
this.finalize();
|
||||||
|
let opos = 0;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
out[opos++] = h[i] >>> 0;
|
||||||
|
out[opos++] = h[i] >>> 8;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const { buffer, outputLen } = this;
|
||||||
|
this.digestInto(buffer);
|
||||||
|
const res = buffer.slice(0, outputLen);
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function wrapConstructorWithKey(hashCons) {
|
||||||
|
const hashC = (msg, key) => hashCons(key).update((0, utils_js_1.toBytes)(msg)).digest();
|
||||||
|
const tmp = hashCons(new Uint8Array(32));
|
||||||
|
hashC.outputLen = tmp.outputLen;
|
||||||
|
hashC.blockLen = tmp.blockLen;
|
||||||
|
hashC.create = (key) => hashCons(key);
|
||||||
|
return hashC;
|
||||||
|
}
|
||||||
|
exports.wrapConstructorWithKey = wrapConstructorWithKey;
|
||||||
|
exports.poly1305 = wrapConstructorWithKey((key) => new Poly1305(key));
|
||||||
|
//# sourceMappingURL=_poly1305.js.map
|
||||||
1
node_modules/@noble/ciphers/_poly1305.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_poly1305.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
27
node_modules/@noble/ciphers/_polyval.d.ts
generated
vendored
Normal file
27
node_modules/@noble/ciphers/_polyval.d.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Input, Hash } from './utils.js';
|
||||||
|
/**
|
||||||
|
* `mulX_POLYVAL(ByteReverse(H))` from spec
|
||||||
|
* @param k mutated in place
|
||||||
|
*/
|
||||||
|
export declare function _toGHASHKey(k: Uint8Array): Uint8Array;
|
||||||
|
export type CHash = ReturnType<typeof wrapConstructorWithKey>;
|
||||||
|
declare function wrapConstructorWithKey<H extends Hash<H>>(hashCons: (key: Input, expectedLength?: number) => Hash<H>): {
|
||||||
|
(msg: Input, key: Input): Uint8Array;
|
||||||
|
outputLen: number;
|
||||||
|
blockLen: number;
|
||||||
|
create(key: Input, expectedLength?: number): Hash<H>;
|
||||||
|
};
|
||||||
|
export declare const ghash: {
|
||||||
|
(msg: Input, key: Input): Uint8Array;
|
||||||
|
outputLen: number;
|
||||||
|
blockLen: number;
|
||||||
|
create(key: Input, expectedLength?: number): Hash<Hash<unknown>>;
|
||||||
|
};
|
||||||
|
export declare const polyval: {
|
||||||
|
(msg: Input, key: Input): Uint8Array;
|
||||||
|
outputLen: number;
|
||||||
|
blockLen: number;
|
||||||
|
create(key: Input, expectedLength?: number): Hash<Hash<unknown>>;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=_polyval.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/_polyval.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_polyval.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_polyval.d.ts","sourceRoot":"","sources":["src/_polyval.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,EAAE,IAAI,EAAO,MAAM,YAAY,CAAC;AAsCnE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAYrD;AA+KD,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC9D,iBAAS,sBAAsB,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,EAC/C,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;UAEtC,KAAK,OAAO,KAAK,GAAG,UAAU;;;gBAK7B,KAAK,mBAAmB,MAAM;EAEpD;AAED,eAAO,MAAM,KAAK;UATI,KAAK,OAAO,KAAK,GAAG,UAAU;;;gBAK7B,KAAK,mBAAmB,MAAM;CAMpD,CAAC;AACF,eAAO,MAAM,OAAO;UAZE,KAAK,OAAO,KAAK,GAAG,UAAU;;;gBAK7B,KAAK,mBAAmB,MAAM;CASpD,CAAC"}
|
||||||
221
node_modules/@noble/ciphers/_polyval.js
generated
vendored
Normal file
221
node_modules/@noble/ciphers/_polyval.js
generated
vendored
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.polyval = exports.ghash = exports._toGHASHKey = void 0;
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
// GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV.
|
||||||
|
// Implemented in terms of GHash with conversion function for keys
|
||||||
|
// GCM GHASH from NIST SP800-38d, SIV from RFC 8452.
|
||||||
|
// https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
||||||
|
// GHASH modulo: x^128 + x^7 + x^2 + x + 1
|
||||||
|
// POLYVAL modulo: x^128 + x^127 + x^126 + x^121 + 1
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
// TODO: rewrite
|
||||||
|
// temporary padding buffer
|
||||||
|
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
||||||
|
const ZEROS32 = (0, utils_js_1.u32)(ZEROS16);
|
||||||
|
const POLY = 0xe1; // v = 2*v % POLY
|
||||||
|
// v = 2*v % POLY
|
||||||
|
// NOTE: because x + x = 0 (add/sub is same), mul2(x) != x+x
|
||||||
|
// We can multiply any number using montgomery ladder and this function (works as double, add is simple xor)
|
||||||
|
const mul2 = (s0, s1, s2, s3) => {
|
||||||
|
const hiBit = s3 & 1;
|
||||||
|
return {
|
||||||
|
s3: (s2 << 31) | (s3 >>> 1),
|
||||||
|
s2: (s1 << 31) | (s2 >>> 1),
|
||||||
|
s1: (s0 << 31) | (s1 >>> 1),
|
||||||
|
s0: (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)), // reduce % poly
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const swapLE = (n) => (((n >>> 0) & 0xff) << 24) |
|
||||||
|
(((n >>> 8) & 0xff) << 16) |
|
||||||
|
(((n >>> 16) & 0xff) << 8) |
|
||||||
|
((n >>> 24) & 0xff) |
|
||||||
|
0;
|
||||||
|
/**
|
||||||
|
* `mulX_POLYVAL(ByteReverse(H))` from spec
|
||||||
|
* @param k mutated in place
|
||||||
|
*/
|
||||||
|
function _toGHASHKey(k) {
|
||||||
|
k.reverse();
|
||||||
|
const hiBit = k[15] & 1;
|
||||||
|
// k >>= 1
|
||||||
|
let carry = 0;
|
||||||
|
for (let i = 0; i < k.length; i++) {
|
||||||
|
const t = k[i];
|
||||||
|
k[i] = (t >>> 1) | carry;
|
||||||
|
carry = (t & 1) << 7;
|
||||||
|
}
|
||||||
|
k[0] ^= -hiBit & 0xe1; // if (hiBit) n ^= 0xe1000000000000000000000000000000;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
exports._toGHASHKey = _toGHASHKey;
|
||||||
|
const estimateWindow = (bytes) => {
|
||||||
|
if (bytes > 64 * 1024)
|
||||||
|
return 8;
|
||||||
|
if (bytes > 1024)
|
||||||
|
return 4;
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
class GHASH {
|
||||||
|
// We select bits per window adaptively based on expectedLength
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
this.blockLen = BLOCK_SIZE;
|
||||||
|
this.outputLen = BLOCK_SIZE;
|
||||||
|
this.s0 = 0;
|
||||||
|
this.s1 = 0;
|
||||||
|
this.s2 = 0;
|
||||||
|
this.s3 = 0;
|
||||||
|
this.finished = false;
|
||||||
|
key = (0, utils_js_1.toBytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(key, 16);
|
||||||
|
const kView = (0, utils_js_1.createView)(key);
|
||||||
|
let k0 = kView.getUint32(0, false);
|
||||||
|
let k1 = kView.getUint32(4, false);
|
||||||
|
let k2 = kView.getUint32(8, false);
|
||||||
|
let k3 = kView.getUint32(12, false);
|
||||||
|
// generate table of doubled keys (half of montgomery ladder)
|
||||||
|
const doubles = [];
|
||||||
|
for (let i = 0; i < 128; i++) {
|
||||||
|
doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) });
|
||||||
|
({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3));
|
||||||
|
}
|
||||||
|
const W = estimateWindow(expectedLength || 1024);
|
||||||
|
if (![1, 2, 4, 8].includes(W))
|
||||||
|
throw new Error(`ghash: wrong window size=${W}, should be 2, 4 or 8`);
|
||||||
|
this.W = W;
|
||||||
|
const bits = 128; // always 128 bits;
|
||||||
|
const windows = bits / W;
|
||||||
|
const windowSize = (this.windowSize = 2 ** W);
|
||||||
|
const items = [];
|
||||||
|
// Create precompute table for window of W bits
|
||||||
|
for (let w = 0; w < windows; w++) {
|
||||||
|
// truth table: 00, 01, 10, 11
|
||||||
|
for (let byte = 0; byte < windowSize; byte++) {
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
||||||
|
for (let j = 0; j < W; j++) {
|
||||||
|
const bit = (byte >>> (W - j - 1)) & 1;
|
||||||
|
if (!bit)
|
||||||
|
continue;
|
||||||
|
const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j];
|
||||||
|
(s0 ^= d0), (s1 ^= d1), (s2 ^= d2), (s3 ^= d3);
|
||||||
|
}
|
||||||
|
items.push({ s0, s1, s2, s3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.t = items;
|
||||||
|
}
|
||||||
|
_updateBlock(s0, s1, s2, s3) {
|
||||||
|
(s0 ^= this.s0), (s1 ^= this.s1), (s2 ^= this.s2), (s3 ^= this.s3);
|
||||||
|
const { W, t, windowSize } = this;
|
||||||
|
// prettier-ignore
|
||||||
|
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
|
||||||
|
const mask = (1 << W) - 1; // 2**W will kill performance.
|
||||||
|
let w = 0;
|
||||||
|
for (const num of [s0, s1, s2, s3]) {
|
||||||
|
for (let bytePos = 0; bytePos < 4; bytePos++) {
|
||||||
|
const byte = (num >>> (8 * bytePos)) & 0xff;
|
||||||
|
for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) {
|
||||||
|
const bit = (byte >>> (W * bitPos)) & mask;
|
||||||
|
const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit];
|
||||||
|
(o0 ^= e0), (o1 ^= e1), (o2 ^= e2), (o3 ^= e3);
|
||||||
|
w += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.s0 = o0;
|
||||||
|
this.s1 = o1;
|
||||||
|
this.s2 = o2;
|
||||||
|
this.s3 = o3;
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
data = (0, utils_js_1.toBytes)(data);
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
const b32 = (0, utils_js_1.u32)(data);
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
this._updateBlock(b32[i * 4 + 0], b32[i * 4 + 1], b32[i * 4 + 2], b32[i * 4 + 3]);
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]);
|
||||||
|
ZEROS32.fill(0); // clean tmp buffer
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
const { t } = this;
|
||||||
|
// clean precompute table
|
||||||
|
for (const elm of t) {
|
||||||
|
(elm.s0 = 0), (elm.s1 = 0), (elm.s2 = 0), (elm.s3 = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
(0, _assert_js_1.output)(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = (0, utils_js_1.u32)(out);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const res = new Uint8Array(BLOCK_SIZE);
|
||||||
|
this.digestInto(res);
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Polyval extends GHASH {
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
key = (0, utils_js_1.toBytes)(key);
|
||||||
|
const ghKey = _toGHASHKey(key.slice());
|
||||||
|
super(ghKey, expectedLength);
|
||||||
|
ghKey.fill(0);
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
data = (0, utils_js_1.toBytes)(data);
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
const b32 = (0, utils_js_1.u32)(data);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
this._updateBlock(swapLE(b32[i * 4 + 3]), swapLE(b32[i * 4 + 2]), swapLE(b32[i * 4 + 1]), swapLE(b32[i * 4 + 0]));
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
this._updateBlock(swapLE(ZEROS32[3]), swapLE(ZEROS32[2]), swapLE(ZEROS32[1]), swapLE(ZEROS32[0]));
|
||||||
|
ZEROS32.fill(0); // clean tmp buffer
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
(0, _assert_js_1.exists)(this);
|
||||||
|
(0, _assert_js_1.output)(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
// tmp ugly hack
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = (0, utils_js_1.u32)(out);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
return out.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function wrapConstructorWithKey(hashCons) {
|
||||||
|
const hashC = (msg, key) => hashCons(key, msg.length).update((0, utils_js_1.toBytes)(msg)).digest();
|
||||||
|
const tmp = hashCons(new Uint8Array(16), 0);
|
||||||
|
hashC.outputLen = tmp.outputLen;
|
||||||
|
hashC.blockLen = tmp.blockLen;
|
||||||
|
hashC.create = (key, expectedLength) => hashCons(key, expectedLength);
|
||||||
|
return hashC;
|
||||||
|
}
|
||||||
|
exports.ghash = wrapConstructorWithKey((key, expectedLength) => new GHASH(key, expectedLength));
|
||||||
|
exports.polyval = wrapConstructorWithKey((key, expectedLength) => new Polyval(key, expectedLength));
|
||||||
|
//# sourceMappingURL=_polyval.js.map
|
||||||
1
node_modules/@noble/ciphers/_polyval.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/_polyval.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
86
node_modules/@noble/ciphers/aes.d.ts
generated
vendored
Normal file
86
node_modules/@noble/ciphers/aes.d.ts
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Cipher, CipherWithOutput } from './utils.js';
|
||||||
|
export declare function expandKeyLE(key: Uint8Array): Uint32Array;
|
||||||
|
export declare function expandKeyDecLE(key: Uint8Array): Uint32Array;
|
||||||
|
declare function encrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number): {
|
||||||
|
s0: number;
|
||||||
|
s1: number;
|
||||||
|
s2: number;
|
||||||
|
s3: number;
|
||||||
|
};
|
||||||
|
declare function decrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number): {
|
||||||
|
s0: number;
|
||||||
|
s1: number;
|
||||||
|
s2: number;
|
||||||
|
s3: number;
|
||||||
|
};
|
||||||
|
declare function ctrCounter(xk: Uint32Array, nonce: Uint8Array, src: Uint8Array, dst?: Uint8Array): Uint8Array;
|
||||||
|
declare function ctr32(xk: Uint32Array, isLE: boolean, nonce: Uint8Array, src: Uint8Array, dst?: Uint8Array): Uint8Array;
|
||||||
|
/**
|
||||||
|
* CTR: counter mode. Creates stream cipher.
|
||||||
|
* Requires good IV. Parallelizable. OK, but no MAC.
|
||||||
|
*/
|
||||||
|
export declare const ctr: ((key: Uint8Array, nonce: Uint8Array) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
};
|
||||||
|
export type BlockOpts = {
|
||||||
|
disablePadding?: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* ECB: Electronic CodeBook. Simple deterministic replacement.
|
||||||
|
* Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/).
|
||||||
|
*/
|
||||||
|
export declare const ecb: ((key: Uint8Array, opts?: BlockOpts) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* CBC: Cipher-Block-Chaining. Key is previous round’s block.
|
||||||
|
* Fragile: needs proper padding. Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
export declare const cbc: ((key: Uint8Array, iv: Uint8Array, opts?: BlockOpts) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* CFB: Cipher Feedback Mode. The input for the block cipher is the previous cipher output.
|
||||||
|
* Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
export declare const cfb: ((key: Uint8Array, iv: Uint8Array) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* GCM: Galois/Counter Mode.
|
||||||
|
* Good, modern version of CTR, parallel, with MAC.
|
||||||
|
* Be careful: MACs can be forged.
|
||||||
|
*/
|
||||||
|
export declare const gcm: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
|
||||||
|
* Guarantees that, when a nonce is repeated, the only security loss is that identical
|
||||||
|
* plaintexts will produce identical ciphertexts.
|
||||||
|
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
|
||||||
|
*/
|
||||||
|
export declare const siv: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => Cipher) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
declare function encryptBlock(xk: Uint32Array, block: Uint8Array): Uint8Array;
|
||||||
|
declare function decryptBlock(xk: Uint32Array, block: Uint8Array): Uint8Array;
|
||||||
|
export declare const unsafe: {
|
||||||
|
expandKeyLE: typeof expandKeyLE;
|
||||||
|
expandKeyDecLE: typeof expandKeyDecLE;
|
||||||
|
encrypt: typeof encrypt;
|
||||||
|
decrypt: typeof decrypt;
|
||||||
|
encryptBlock: typeof encryptBlock;
|
||||||
|
decryptBlock: typeof decryptBlock;
|
||||||
|
ctrCounter: typeof ctrCounter;
|
||||||
|
ctr32: typeof ctr32;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=aes.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/aes.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/aes.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"aes.d.ts","sourceRoot":"","sources":["src/aes.ts"],"names":[],"mappings":"AACA,OAAO,EACO,MAAM,EAAE,gBAAgB,EAErC,MAAM,YAAY,CAAC;AAmGpB,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,CAmBxD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,CAkB3D;AAwBD,iBAAS,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;;;;EAkB/E;AAED,iBAAS,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;;;;EAkB/E;AAWD,iBAAS,UAAU,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,UAAU,cAmCxF;AAKD,iBAAS,KAAK,CACZ,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,UAAU,EACf,GAAG,CAAC,EAAE,UAAU,cAiCjB;AAED;;;GAGG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,SAAS,UAAU,KAAG,gBAAgB;;;CAgBnE,CAAC;AAgDF,MAAM,MAAM,SAAS,GAAG;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,SAAQ,SAAS,KAAQ,gBAAgB;;CAoCtE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,MAAM,UAAU,SAAQ,SAAS,KAAQ,gBAAgB;;;CA+CtF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,MAAM,UAAU,KAAG,gBAAgB;;;CAqChE,CAAC;AAqBF;;;;GAIG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,MAAM;;;;CAyD3E,CAAC;AAOF;;;;;GAKG;AACH,eAAO,MAAM,GAAG,SAEI,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,MAAM;;;;CAqF3E,CAAC;AAUF,iBAAS,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,cAOvD;AAED,iBAAS,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,cAOvD;AAID,eAAO,MAAM,MAAM;;;;;;;;;CASlB,CAAC"}
|
||||||
675
node_modules/@noble/ciphers/aes.js
generated
vendored
Normal file
675
node_modules/@noble/ciphers/aes.js
generated
vendored
Normal file
@@ -0,0 +1,675 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.unsafe = exports.siv = exports.gcm = exports.cfb = exports.cbc = exports.ecb = exports.ctr = exports.expandKeyDecLE = exports.expandKeyLE = void 0;
|
||||||
|
// prettier-ignore
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
const _polyval_js_1 = require("./_polyval.js");
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
/*
|
||||||
|
AES (Advanced Encryption Standard) aka Rijndael block cipher.
|
||||||
|
|
||||||
|
Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256 bits). In every round:
|
||||||
|
1. **S-box**, table substitution
|
||||||
|
2. **Shift rows**, cyclic shift left of all rows of data array
|
||||||
|
3. **Mix columns**, multiplying every column by fixed polynomial
|
||||||
|
4. **Add round key**, round_key xor i-th column of array
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
- FIPS-197 https://csrc.nist.gov/files/pubs/fips/197/final/docs/fips-197.pdf
|
||||||
|
- Original proposal: https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf
|
||||||
|
*/
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
const BLOCK_SIZE32 = 4;
|
||||||
|
const EMPTY_BLOCK = new Uint8Array(BLOCK_SIZE);
|
||||||
|
const POLY = 0x11b; // 1 + x + x**3 + x**4 + x**8
|
||||||
|
// TODO: remove multiplication, binary ops only
|
||||||
|
function mul2(n) {
|
||||||
|
return (n << 1) ^ (POLY & -(n >> 7));
|
||||||
|
}
|
||||||
|
function mul(a, b) {
|
||||||
|
let res = 0;
|
||||||
|
for (; b > 0; b >>= 1) {
|
||||||
|
// Montgomery ladder
|
||||||
|
res ^= a & -(b & 1); // if (b&1) res ^=a (but const-time).
|
||||||
|
a = mul2(a); // a = 2*a
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// AES S-box is generated using finite field inversion,
|
||||||
|
// an affine transform, and xor of a constant 0x63.
|
||||||
|
const sbox = /* @__PURE__ */ (() => {
|
||||||
|
let t = new Uint8Array(256);
|
||||||
|
for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x))
|
||||||
|
t[i] = x;
|
||||||
|
const box = new Uint8Array(256);
|
||||||
|
box[0] = 0x63; // first elm
|
||||||
|
for (let i = 0; i < 255; i++) {
|
||||||
|
let x = t[255 - i];
|
||||||
|
x |= x << 8;
|
||||||
|
box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff;
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
})();
|
||||||
|
// Inverted S-box
|
||||||
|
const invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
|
||||||
|
// Rotate u32 by 8
|
||||||
|
const rotr32_8 = (n) => (n << 24) | (n >>> 8);
|
||||||
|
const rotl32_8 = (n) => (n << 8) | (n >>> 24);
|
||||||
|
// T-table is optimization suggested in 5.2 of original proposal (missed from FIPS-197). Changes:
|
||||||
|
// - LE instead of BE
|
||||||
|
// - bigger tables: T0 and T1 are merged into T01 table and T2 & T3 into T23;
|
||||||
|
// so index is u16, instead of u8. This speeds up things, unexpectedly
|
||||||
|
function genTtable(sbox, fn) {
|
||||||
|
if (sbox.length !== 256)
|
||||||
|
throw new Error('Wrong sbox length');
|
||||||
|
const T0 = new Uint32Array(256).map((_, j) => fn(sbox[j]));
|
||||||
|
const T1 = T0.map(rotl32_8);
|
||||||
|
const T2 = T1.map(rotl32_8);
|
||||||
|
const T3 = T2.map(rotl32_8);
|
||||||
|
const T01 = new Uint32Array(256 * 256);
|
||||||
|
const T23 = new Uint32Array(256 * 256);
|
||||||
|
const sbox2 = new Uint16Array(256 * 256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
for (let j = 0; j < 256; j++) {
|
||||||
|
const idx = i * 256 + j;
|
||||||
|
T01[idx] = T0[i] ^ T1[j];
|
||||||
|
T23[idx] = T2[i] ^ T3[j];
|
||||||
|
sbox2[idx] = (sbox[i] << 8) | sbox[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { sbox, sbox2, T0, T1, T2, T3, T01, T23 };
|
||||||
|
}
|
||||||
|
const tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => (mul(s, 3) << 24) | (s << 16) | (s << 8) | mul(s, 2));
|
||||||
|
const tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => (mul(s, 11) << 24) | (mul(s, 13) << 16) | (mul(s, 9) << 8) | mul(s, 14));
|
||||||
|
const xPowers = /* @__PURE__ */ (() => {
|
||||||
|
const p = new Uint8Array(16);
|
||||||
|
for (let i = 0, x = 1; i < 16; i++, x = mul2(x))
|
||||||
|
p[i] = x;
|
||||||
|
return p;
|
||||||
|
})();
|
||||||
|
function expandKeyLE(key) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
const len = key.length;
|
||||||
|
if (![16, 24, 32].includes(len))
|
||||||
|
throw new Error(`aes: wrong key size: should be 16, 24 or 32, got: ${len}`);
|
||||||
|
const { sbox2 } = tableEncoding;
|
||||||
|
const k32 = (0, utils_js_1.u32)(key);
|
||||||
|
const Nk = k32.length;
|
||||||
|
const subByte = (n) => applySbox(sbox2, n, n, n, n);
|
||||||
|
const xk = new Uint32Array(len + 28); // expanded key
|
||||||
|
xk.set(k32);
|
||||||
|
// 4.3.1 Key expansion
|
||||||
|
for (let i = Nk; i < xk.length; i++) {
|
||||||
|
let t = xk[i - 1];
|
||||||
|
if (i % Nk === 0)
|
||||||
|
t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1];
|
||||||
|
else if (Nk > 6 && i % Nk === 4)
|
||||||
|
t = subByte(t);
|
||||||
|
xk[i] = xk[i - Nk] ^ t;
|
||||||
|
}
|
||||||
|
return xk;
|
||||||
|
}
|
||||||
|
exports.expandKeyLE = expandKeyLE;
|
||||||
|
function expandKeyDecLE(key) {
|
||||||
|
const encKey = expandKeyLE(key);
|
||||||
|
const xk = encKey.slice();
|
||||||
|
const Nk = encKey.length;
|
||||||
|
const { sbox2 } = tableEncoding;
|
||||||
|
const { T0, T1, T2, T3 } = tableDecoding;
|
||||||
|
// Inverse key by chunks of 4 (rounds)
|
||||||
|
for (let i = 0; i < Nk; i += 4) {
|
||||||
|
for (let j = 0; j < 4; j++)
|
||||||
|
xk[i + j] = encKey[Nk - i - 4 + j];
|
||||||
|
}
|
||||||
|
encKey.fill(0);
|
||||||
|
// apply InvMixColumn except first & last round
|
||||||
|
for (let i = 4; i < Nk - 4; i++) {
|
||||||
|
const x = xk[i];
|
||||||
|
const w = applySbox(sbox2, x, x, x, x);
|
||||||
|
xk[i] = T0[w & 0xff] ^ T1[(w >>> 8) & 0xff] ^ T2[(w >>> 16) & 0xff] ^ T3[w >>> 24];
|
||||||
|
}
|
||||||
|
return xk;
|
||||||
|
}
|
||||||
|
exports.expandKeyDecLE = expandKeyDecLE;
|
||||||
|
// Apply tables
|
||||||
|
function apply0123(T01, T23, s0, s1, s2, s3) {
|
||||||
|
return (T01[((s0 << 8) & 0xff00) | ((s1 >>> 8) & 0xff)] ^
|
||||||
|
T23[((s2 >>> 8) & 0xff00) | ((s3 >>> 24) & 0xff)]);
|
||||||
|
}
|
||||||
|
function applySbox(sbox2, s0, s1, s2, s3) {
|
||||||
|
return (sbox2[(s0 & 0xff) | (s1 & 0xff00)] |
|
||||||
|
(sbox2[((s2 >>> 16) & 0xff) | ((s3 >>> 16) & 0xff00)] << 16));
|
||||||
|
}
|
||||||
|
function encrypt(xk, s0, s1, s2, s3) {
|
||||||
|
const { sbox2, T01, T23 } = tableEncoding;
|
||||||
|
let k = 0;
|
||||||
|
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||||||
|
const rounds = xk.length / 4 - 2;
|
||||||
|
for (let i = 0; i < rounds; i++) {
|
||||||
|
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3);
|
||||||
|
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0);
|
||||||
|
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1);
|
||||||
|
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2);
|
||||||
|
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||||||
|
}
|
||||||
|
// last round (without mixcolumns, so using SBOX2 table)
|
||||||
|
const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3);
|
||||||
|
const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0);
|
||||||
|
const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1);
|
||||||
|
const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2);
|
||||||
|
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||||||
|
}
|
||||||
|
function decrypt(xk, s0, s1, s2, s3) {
|
||||||
|
const { sbox2, T01, T23 } = tableDecoding;
|
||||||
|
let k = 0;
|
||||||
|
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||||||
|
const rounds = xk.length / 4 - 2;
|
||||||
|
for (let i = 0; i < rounds; i++) {
|
||||||
|
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s3, s2, s1);
|
||||||
|
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s0, s3, s2);
|
||||||
|
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s1, s0, s3);
|
||||||
|
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s2, s1, s0);
|
||||||
|
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||||||
|
}
|
||||||
|
// Last round
|
||||||
|
const t0 = xk[k++] ^ applySbox(sbox2, s0, s3, s2, s1);
|
||||||
|
const t1 = xk[k++] ^ applySbox(sbox2, s1, s0, s3, s2);
|
||||||
|
const t2 = xk[k++] ^ applySbox(sbox2, s2, s1, s0, s3);
|
||||||
|
const t3 = xk[k++] ^ applySbox(sbox2, s3, s2, s1, s0);
|
||||||
|
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||||||
|
}
|
||||||
|
function getDst(len, dst) {
|
||||||
|
if (!dst)
|
||||||
|
return new Uint8Array(len);
|
||||||
|
(0, _assert_js_1.bytes)(dst);
|
||||||
|
if (dst.length < len)
|
||||||
|
throw new Error(`aes: wrong destination length, expected at least ${len}, got: ${dst.length}`);
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
// TODO: investigate merging with ctr32
|
||||||
|
function ctrCounter(xk, nonce, src, dst) {
|
||||||
|
(0, _assert_js_1.bytes)(nonce, BLOCK_SIZE);
|
||||||
|
(0, _assert_js_1.bytes)(src);
|
||||||
|
const srcLen = src.length;
|
||||||
|
dst = getDst(srcLen, dst);
|
||||||
|
const ctr = nonce;
|
||||||
|
const c32 = (0, utils_js_1.u32)(ctr);
|
||||||
|
// Fill block (empty, ctr=0)
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||||||
|
const src32 = (0, utils_js_1.u32)(src);
|
||||||
|
const dst32 = (0, utils_js_1.u32)(dst);
|
||||||
|
// process blocks
|
||||||
|
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ s0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ s1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ s2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ s3;
|
||||||
|
// Full 128 bit counter with wrap around
|
||||||
|
let carry = 1;
|
||||||
|
for (let i = ctr.length - 1; i >= 0; i--) {
|
||||||
|
carry = (carry + (ctr[i] & 0xff)) | 0;
|
||||||
|
ctr[i] = carry & 0xff;
|
||||||
|
carry >>>= 8;
|
||||||
|
}
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||||||
|
}
|
||||||
|
// leftovers (less than block)
|
||||||
|
// It's possible to handle > u32 fast, but is it worth it?
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||||||
|
const buf = (0, utils_js_1.u8)(b32);
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
// AES CTR with overflowing 32 bit counter
|
||||||
|
// It's possible to do 32le significantly simpler (and probably faster) by using u32.
|
||||||
|
// But, we need both, and perf bottleneck is in ghash anyway.
|
||||||
|
function ctr32(xk, isLE, nonce, src, dst) {
|
||||||
|
(0, _assert_js_1.bytes)(nonce, BLOCK_SIZE);
|
||||||
|
(0, _assert_js_1.bytes)(src);
|
||||||
|
dst = getDst(src.length, dst);
|
||||||
|
const ctr = nonce; // write new value to nonce, so it can be re-used
|
||||||
|
const c32 = (0, utils_js_1.u32)(ctr);
|
||||||
|
const view = (0, utils_js_1.createView)(ctr);
|
||||||
|
const src32 = (0, utils_js_1.u32)(src);
|
||||||
|
const dst32 = (0, utils_js_1.u32)(dst);
|
||||||
|
const ctrPos = isLE ? 0 : 12;
|
||||||
|
const srcLen = src.length;
|
||||||
|
// Fill block (empty, ctr=0)
|
||||||
|
let ctrNum = view.getUint32(ctrPos, isLE); // read current counter value
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||||||
|
// process blocks
|
||||||
|
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ s0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ s1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ s2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ s3;
|
||||||
|
ctrNum = (ctrNum + 1) >>> 0; // u32 wrap
|
||||||
|
view.setUint32(ctrPos, ctrNum, isLE);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||||||
|
}
|
||||||
|
// leftovers (less than a block)
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||||||
|
const buf = (0, utils_js_1.u8)(b32);
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* CTR: counter mode. Creates stream cipher.
|
||||||
|
* Requires good IV. Parallelizable. OK, but no MAC.
|
||||||
|
*/
|
||||||
|
exports.ctr = (0, utils_js_1.wrapCipher)({ blockSize: 16, nonceLength: 16 }, function ctr(key, nonce) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(nonce, BLOCK_SIZE);
|
||||||
|
function processCtr(buf, dst) {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const n = nonce.slice();
|
||||||
|
const out = ctrCounter(xk, n, buf, dst);
|
||||||
|
xk.fill(0);
|
||||||
|
n.fill(0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => processCtr(plaintext, dst),
|
||||||
|
decrypt: (ciphertext, dst) => processCtr(ciphertext, dst),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
function validateBlockDecrypt(data) {
|
||||||
|
(0, _assert_js_1.bytes)(data);
|
||||||
|
if (data.length % BLOCK_SIZE !== 0) {
|
||||||
|
throw new Error(`aes/(cbc-ecb).decrypt ciphertext should consist of blocks with size ${BLOCK_SIZE}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateBlockEncrypt(plaintext, pcks5, dst) {
|
||||||
|
let outLen = plaintext.length;
|
||||||
|
const remaining = outLen % BLOCK_SIZE;
|
||||||
|
if (!pcks5 && remaining !== 0)
|
||||||
|
throw new Error('aec/(cbc-ecb): unpadded plaintext with disabled padding');
|
||||||
|
const b = (0, utils_js_1.u32)(plaintext);
|
||||||
|
if (pcks5) {
|
||||||
|
let left = BLOCK_SIZE - remaining;
|
||||||
|
if (!left)
|
||||||
|
left = BLOCK_SIZE; // if no bytes left, create empty padding block
|
||||||
|
outLen = outLen + left;
|
||||||
|
}
|
||||||
|
const out = getDst(outLen, dst);
|
||||||
|
const o = (0, utils_js_1.u32)(out);
|
||||||
|
return { b, o, out };
|
||||||
|
}
|
||||||
|
function validatePCKS(data, pcks5) {
|
||||||
|
if (!pcks5)
|
||||||
|
return data;
|
||||||
|
const len = data.length;
|
||||||
|
if (!len)
|
||||||
|
throw new Error(`aes/pcks5: empty ciphertext not allowed`);
|
||||||
|
const lastByte = data[len - 1];
|
||||||
|
if (lastByte <= 0 || lastByte > 16)
|
||||||
|
throw new Error(`aes/pcks5: wrong padding byte: ${lastByte}`);
|
||||||
|
const out = data.subarray(0, -lastByte);
|
||||||
|
for (let i = 0; i < lastByte; i++)
|
||||||
|
if (data[len - i - 1] !== lastByte)
|
||||||
|
throw new Error(`aes/pcks5: wrong padding`);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function padPCKS(left) {
|
||||||
|
const tmp = new Uint8Array(16);
|
||||||
|
const tmp32 = (0, utils_js_1.u32)(tmp);
|
||||||
|
tmp.set(left);
|
||||||
|
const paddingByte = BLOCK_SIZE - left.length;
|
||||||
|
for (let i = BLOCK_SIZE - paddingByte; i < BLOCK_SIZE; i++)
|
||||||
|
tmp[i] = paddingByte;
|
||||||
|
return tmp32;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* ECB: Electronic CodeBook. Simple deterministic replacement.
|
||||||
|
* Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/).
|
||||||
|
*/
|
||||||
|
exports.ecb = (0, utils_js_1.wrapCipher)({ blockSize: 16 }, function ecb(key, opts = {}) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
const pcks5 = !opts.disablePadding;
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => {
|
||||||
|
(0, _assert_js_1.bytes)(plaintext);
|
||||||
|
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
let i = 0;
|
||||||
|
for (; i + 4 <= b.length;) {
|
||||||
|
const { s0, s1, s2, s3 } = encrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
if (pcks5) {
|
||||||
|
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||||||
|
const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return _out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, dst) => {
|
||||||
|
validateBlockDecrypt(ciphertext);
|
||||||
|
const xk = expandKeyDecLE(key);
|
||||||
|
const out = getDst(ciphertext.length, dst);
|
||||||
|
const b = (0, utils_js_1.u32)(ciphertext);
|
||||||
|
const o = (0, utils_js_1.u32)(out);
|
||||||
|
for (let i = 0; i + 4 <= b.length;) {
|
||||||
|
const { s0, s1, s2, s3 } = decrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return validatePCKS(out, pcks5);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* CBC: Cipher-Block-Chaining. Key is previous round’s block.
|
||||||
|
* Fragile: needs proper padding. Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
exports.cbc = (0, utils_js_1.wrapCipher)({ blockSize: 16, nonceLength: 16 }, function cbc(key, iv, opts = {}) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(iv, 16);
|
||||||
|
const pcks5 = !opts.disablePadding;
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||||||
|
const n32 = (0, utils_js_1.u32)(iv);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
let i = 0;
|
||||||
|
for (; i + 4 <= b.length;) {
|
||||||
|
(s0 ^= b[i + 0]), (s1 ^= b[i + 1]), (s2 ^= b[i + 2]), (s3 ^= b[i + 3]);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
if (pcks5) {
|
||||||
|
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||||||
|
(s0 ^= tmp32[0]), (s1 ^= tmp32[1]), (s2 ^= tmp32[2]), (s3 ^= tmp32[3]);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return _out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, dst) => {
|
||||||
|
validateBlockDecrypt(ciphertext);
|
||||||
|
const xk = expandKeyDecLE(key);
|
||||||
|
const n32 = (0, utils_js_1.u32)(iv);
|
||||||
|
const out = getDst(ciphertext.length, dst);
|
||||||
|
const b = (0, utils_js_1.u32)(ciphertext);
|
||||||
|
const o = (0, utils_js_1.u32)(out);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
for (let i = 0; i + 4 <= b.length;) {
|
||||||
|
// prettier-ignore
|
||||||
|
const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3;
|
||||||
|
(s0 = b[i + 0]), (s1 = b[i + 1]), (s2 = b[i + 2]), (s3 = b[i + 3]);
|
||||||
|
const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3);
|
||||||
|
(o[i++] = o0 ^ ps0), (o[i++] = o1 ^ ps1), (o[i++] = o2 ^ ps2), (o[i++] = o3 ^ ps3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return validatePCKS(out, pcks5);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* CFB: Cipher Feedback Mode. The input for the block cipher is the previous cipher output.
|
||||||
|
* Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
exports.cfb = (0, utils_js_1.wrapCipher)({ blockSize: 16, nonceLength: 16 }, function cfb(key, iv) {
|
||||||
|
(0, _assert_js_1.bytes)(key);
|
||||||
|
(0, _assert_js_1.bytes)(iv, 16);
|
||||||
|
function processCfb(src, isEncrypt, dst) {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const srcLen = src.length;
|
||||||
|
dst = getDst(srcLen, dst);
|
||||||
|
const src32 = (0, utils_js_1.u32)(src);
|
||||||
|
const dst32 = (0, utils_js_1.u32)(dst);
|
||||||
|
const next32 = isEncrypt ? dst32 : src32;
|
||||||
|
const n32 = (0, utils_js_1.u32)(iv);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
for (let i = 0; i + 4 <= src32.length;) {
|
||||||
|
const { s0: e0, s1: e1, s2: e2, s3: e3 } = encrypt(xk, s0, s1, s2, s3);
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ e0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ e1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ e2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ e3;
|
||||||
|
(s0 = next32[i++]), (s1 = next32[i++]), (s2 = next32[i++]), (s3 = next32[i++]);
|
||||||
|
}
|
||||||
|
// leftovers (less than block)
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
const buf = (0, utils_js_1.u8)(new Uint32Array([s0, s1, s2, s3]));
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
buf.fill(0);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => processCfb(plaintext, true, dst),
|
||||||
|
decrypt: (ciphertext, dst) => processCfb(ciphertext, false, dst),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// TODO: merge with chacha, however gcm has bitLen while chacha has byteLen
|
||||||
|
function computeTag(fn, isLE, key, data, AAD) {
|
||||||
|
const h = fn.create(key, data.length + (AAD?.length || 0));
|
||||||
|
if (AAD)
|
||||||
|
h.update(AAD);
|
||||||
|
h.update(data);
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = (0, utils_js_1.createView)(num);
|
||||||
|
if (AAD)
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 0, BigInt(AAD.length * 8), isLE);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 8, BigInt(data.length * 8), isLE);
|
||||||
|
h.update(num);
|
||||||
|
return h.digest();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* GCM: Galois/Counter Mode.
|
||||||
|
* Good, modern version of CTR, parallel, with MAC.
|
||||||
|
* Be careful: MACs can be forged.
|
||||||
|
*/
|
||||||
|
exports.gcm = (0, utils_js_1.wrapCipher)({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function gcm(key, nonce, AAD) {
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
// Nonce can be pretty much anything (even 1 byte). But smaller nonces less secure.
|
||||||
|
if (nonce.length === 0)
|
||||||
|
throw new Error('aes/gcm: empty nonce');
|
||||||
|
const tagLength = 16;
|
||||||
|
function _computeTag(authKey, tagMask, data) {
|
||||||
|
const tag = computeTag(_polyval_js_1.ghash, false, authKey, data, AAD);
|
||||||
|
for (let i = 0; i < tagMask.length; i++)
|
||||||
|
tag[i] ^= tagMask[i];
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
function deriveKeys() {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const authKey = EMPTY_BLOCK.slice();
|
||||||
|
const counter = EMPTY_BLOCK.slice();
|
||||||
|
ctr32(xk, false, counter, counter, authKey);
|
||||||
|
if (nonce.length === 12) {
|
||||||
|
counter.set(nonce);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Spec (NIST 800-38d) supports variable size nonce.
|
||||||
|
// Not supported for now, but can be useful.
|
||||||
|
const nonceLen = EMPTY_BLOCK.slice();
|
||||||
|
const view = (0, utils_js_1.createView)(nonceLen);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 8, BigInt(nonce.length * 8), false);
|
||||||
|
// ghash(nonce || u64be(0) || u64be(nonceLen*8))
|
||||||
|
_polyval_js_1.ghash.create(authKey).update(nonce).update(nonceLen).digestInto(counter);
|
||||||
|
}
|
||||||
|
const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK);
|
||||||
|
return { xk, authKey, counter, tagMask };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
(0, _assert_js_1.bytes)(plaintext);
|
||||||
|
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||||||
|
const out = new Uint8Array(plaintext.length + tagLength);
|
||||||
|
ctr32(xk, false, counter, plaintext, out);
|
||||||
|
const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength));
|
||||||
|
out.set(tag, plaintext.length);
|
||||||
|
xk.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
(0, _assert_js_1.bytes)(ciphertext);
|
||||||
|
if (ciphertext.length < tagLength)
|
||||||
|
throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`);
|
||||||
|
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const tag = _computeTag(authKey, tagMask, data);
|
||||||
|
if (!(0, utils_js_1.equalBytes)(tag, passedTag))
|
||||||
|
throw new Error('aes/gcm: invalid ghash tag');
|
||||||
|
const out = ctr32(xk, false, counter, data);
|
||||||
|
authKey.fill(0);
|
||||||
|
tagMask.fill(0);
|
||||||
|
xk.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const limit = (name, min, max) => (value) => {
|
||||||
|
if (!Number.isSafeInteger(value) || min > value || value > max)
|
||||||
|
throw new Error(`${name}: invalid value=${value}, must be [${min}..${max}]`);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
|
||||||
|
* Guarantees that, when a nonce is repeated, the only security loss is that identical
|
||||||
|
* plaintexts will produce identical ciphertexts.
|
||||||
|
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
|
||||||
|
*/
|
||||||
|
exports.siv = (0, utils_js_1.wrapCipher)({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function siv(key, nonce, AAD) {
|
||||||
|
const tagLength = 16;
|
||||||
|
// From RFC 8452: Section 6
|
||||||
|
const AAD_LIMIT = limit('AAD', 0, 2 ** 36);
|
||||||
|
const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36);
|
||||||
|
const NONCE_LIMIT = limit('nonce', 12, 12);
|
||||||
|
const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16);
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
NONCE_LIMIT(nonce.length);
|
||||||
|
if (AAD) {
|
||||||
|
(0, _assert_js_1.bytes)(AAD);
|
||||||
|
AAD_LIMIT(AAD.length);
|
||||||
|
}
|
||||||
|
function deriveKeys() {
|
||||||
|
const len = key.length;
|
||||||
|
if (len !== 16 && len !== 24 && len !== 32)
|
||||||
|
throw new Error(`key length must be 16, 24 or 32 bytes, got: ${len} bytes`);
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const encKey = new Uint8Array(len);
|
||||||
|
const authKey = new Uint8Array(16);
|
||||||
|
const n32 = (0, utils_js_1.u32)(nonce);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2];
|
||||||
|
let counter = 0;
|
||||||
|
for (const derivedKey of [authKey, encKey].map(utils_js_1.u32)) {
|
||||||
|
const d32 = (0, utils_js_1.u32)(derivedKey);
|
||||||
|
for (let i = 0; i < d32.length; i += 2) {
|
||||||
|
// aes(u32le(0) || nonce)[:8] || aes(u32le(1) || nonce)[:8] ...
|
||||||
|
const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3);
|
||||||
|
d32[i + 0] = o0;
|
||||||
|
d32[i + 1] = o1;
|
||||||
|
s0 = ++counter; // increment counter inside state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return { authKey, encKey: expandKeyLE(encKey) };
|
||||||
|
}
|
||||||
|
function _computeTag(encKey, authKey, data) {
|
||||||
|
const tag = computeTag(_polyval_js_1.polyval, true, authKey, data, AAD);
|
||||||
|
// Compute the expected tag by XORing S_s and the nonce, clearing the
|
||||||
|
// most significant bit of the last byte and encrypting with the
|
||||||
|
// message-encryption key.
|
||||||
|
for (let i = 0; i < 12; i++)
|
||||||
|
tag[i] ^= nonce[i];
|
||||||
|
tag[15] &= 0x7f; // Clear the highest bit
|
||||||
|
// encrypt tag as block
|
||||||
|
const t32 = (0, utils_js_1.u32)(tag);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3];
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3));
|
||||||
|
(t32[0] = s0), (t32[1] = s1), (t32[2] = s2), (t32[3] = s3);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
// actual decrypt/encrypt of message.
|
||||||
|
function processSiv(encKey, tag, input) {
|
||||||
|
let block = tag.slice();
|
||||||
|
block[15] |= 0x80; // Force highest bit
|
||||||
|
return ctr32(encKey, true, block, input);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
(0, _assert_js_1.bytes)(plaintext);
|
||||||
|
PLAIN_LIMIT(plaintext.length);
|
||||||
|
const { encKey, authKey } = deriveKeys();
|
||||||
|
const tag = _computeTag(encKey, authKey, plaintext);
|
||||||
|
const out = new Uint8Array(plaintext.length + tagLength);
|
||||||
|
out.set(tag, plaintext.length);
|
||||||
|
out.set(processSiv(encKey, tag, plaintext));
|
||||||
|
encKey.fill(0);
|
||||||
|
authKey.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
(0, _assert_js_1.bytes)(ciphertext);
|
||||||
|
CIPHER_LIMIT(ciphertext.length);
|
||||||
|
const tag = ciphertext.subarray(-tagLength);
|
||||||
|
const { encKey, authKey } = deriveKeys();
|
||||||
|
const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength));
|
||||||
|
const expectedTag = _computeTag(encKey, authKey, plaintext);
|
||||||
|
encKey.fill(0);
|
||||||
|
authKey.fill(0);
|
||||||
|
if (!(0, utils_js_1.equalBytes)(tag, expectedTag))
|
||||||
|
throw new Error('invalid polyval tag');
|
||||||
|
return plaintext;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
function isBytes32(a) {
|
||||||
|
return (a != null &&
|
||||||
|
typeof a === 'object' &&
|
||||||
|
(a instanceof Uint32Array || a.constructor.name === 'Uint32Array'));
|
||||||
|
}
|
||||||
|
function encryptBlock(xk, block) {
|
||||||
|
(0, _assert_js_1.bytes)(block, 16);
|
||||||
|
if (!isBytes32(xk))
|
||||||
|
throw new Error('_encryptBlock accepts result of expandKeyLE');
|
||||||
|
const b32 = (0, utils_js_1.u32)(block);
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||||||
|
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
function decryptBlock(xk, block) {
|
||||||
|
(0, _assert_js_1.bytes)(block, 16);
|
||||||
|
if (!isBytes32(xk))
|
||||||
|
throw new Error('_decryptBlock accepts result of expandKeyLE');
|
||||||
|
const b32 = (0, utils_js_1.u32)(block);
|
||||||
|
let { s0, s1, s2, s3 } = decrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||||||
|
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
// Highly unsafe private functions for implementing new modes or ciphers based on AES
|
||||||
|
// Can change at any time, no API guarantees
|
||||||
|
exports.unsafe = {
|
||||||
|
expandKeyLE,
|
||||||
|
expandKeyDecLE,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
encryptBlock,
|
||||||
|
decryptBlock,
|
||||||
|
ctrCounter,
|
||||||
|
ctr32,
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=aes.js.map
|
||||||
1
node_modules/@noble/ciphers/aes.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/aes.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
61
node_modules/@noble/ciphers/chacha.d.ts
generated
vendored
Normal file
61
node_modules/@noble/ciphers/chacha.d.ts
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { CipherWithOutput, XorStream } from './utils.js';
|
||||||
|
/**
|
||||||
|
* hchacha helper method, used primarily in xchacha, to hash
|
||||||
|
* key and nonce into key' and nonce'.
|
||||||
|
* Same as chachaCore, but there doesn't seem to be a way to move the block
|
||||||
|
* out without 25% performance hit.
|
||||||
|
*/
|
||||||
|
export declare function hchacha(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array): void;
|
||||||
|
/**
|
||||||
|
* Original, non-RFC chacha20 from DJB. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
export declare const chacha20orig: XorStream;
|
||||||
|
/**
|
||||||
|
* ChaCha stream cipher. Conforms to RFC 8439 (IETF, TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
export declare const chacha20: XorStream;
|
||||||
|
/**
|
||||||
|
* XChaCha eXtended-nonce ChaCha. 24-byte nonce.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
export declare const xchacha20: XorStream;
|
||||||
|
/**
|
||||||
|
* Reduced 8-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
export declare const chacha8: XorStream;
|
||||||
|
/**
|
||||||
|
* Reduced 12-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
export declare const chacha12: XorStream;
|
||||||
|
/**
|
||||||
|
* AEAD algorithm from RFC 8439.
|
||||||
|
* Salsa20 and chacha (RFC 8439) use poly1305 differently.
|
||||||
|
* We could have composed them similar to:
|
||||||
|
* https://github.com/paulmillr/scure-base/blob/b266c73dde977b1dd7ef40ef7a23cc15aab526b3/index.ts#L250
|
||||||
|
* But it's hard because of authKey:
|
||||||
|
* In salsa20, authKey changes position in salsa stream.
|
||||||
|
* In chacha, authKey can't be computed inside computeTag, it modifies the counter.
|
||||||
|
*/
|
||||||
|
export declare const _poly1305_aead: (xorStream: XorStream) => (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => CipherWithOutput;
|
||||||
|
/**
|
||||||
|
* ChaCha20-Poly1305 from RFC 8439.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
export declare const chacha20poly1305: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* XChaCha20-Poly1305 extended-nonce chacha.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
export declare const xchacha20poly1305: ((key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => CipherWithOutput) & {
|
||||||
|
blockSize: number;
|
||||||
|
nonceLength: number;
|
||||||
|
tagLength: number;
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=chacha.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/chacha.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/chacha.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chacha.d.ts","sourceRoot":"","sources":["src/chacha.ts"],"names":[],"mappings":"AACA,OAAO,EACO,gBAAgB,EAAE,SAAS,EACxC,MAAM,YAAY,CAAC;AA6EpB;;;;;GAKG;AAEH,wBAAgB,OAAO,CACrB,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,QAoDjE;AACD;;GAEG;AACH,eAAO,MAAM,YAAY,WAIvB,CAAC;AACH;;;GAGG;AACH,eAAO,MAAM,QAAQ,WAInB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,SAAS,WAKpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,OAAO,WAIlB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,QAAQ,WAInB,CAAC;AAgCH;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,cACb,SAAS,WACf,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,gBAoCvD,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,gBAAgB,SA1CrB,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,gBAAgB;;;;CA6CzE,CAAC;AACF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,SAnDtB,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAG,gBAAgB;;;;CAsDzE,CAAC"}
|
||||||
323
node_modules/@noble/ciphers/chacha.js
generated
vendored
Normal file
323
node_modules/@noble/ciphers/chacha.js
generated
vendored
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.xchacha20poly1305 = exports.chacha20poly1305 = exports._poly1305_aead = exports.chacha12 = exports.chacha8 = exports.xchacha20 = exports.chacha20 = exports.chacha20orig = exports.hchacha = void 0;
|
||||||
|
// prettier-ignore
|
||||||
|
const utils_js_1 = require("./utils.js");
|
||||||
|
const _poly1305_js_1 = require("./_poly1305.js");
|
||||||
|
const _arx_js_1 = require("./_arx.js");
|
||||||
|
const _assert_js_1 = require("./_assert.js");
|
||||||
|
// ChaCha20 stream cipher was released in 2008. ChaCha aims to increase
|
||||||
|
// the diffusion per round, but had slightly less cryptanalysis.
|
||||||
|
// https://cr.yp.to/chacha.html, http://cr.yp.to/chacha/chacha-20080128.pdf
|
||||||
|
/**
|
||||||
|
* ChaCha core function.
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], // "expa" "nd 3" "2-by" "te k"
|
||||||
|
y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key
|
||||||
|
y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key
|
||||||
|
y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce
|
||||||
|
// Save state to temporary variables
|
||||||
|
let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x00, 16);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x08, 12);
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x00, 8);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x08, 7);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x01, 16);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x09, 12);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x01, 8);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x09, 7);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x02, 16);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x10, 12);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x02, 8);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x10, 7);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x03, 16);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x11, 12);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x03, 8);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x11, 7);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x00, 16);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x10, 12);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x00, 8);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x10, 7);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x01, 16);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x11, 12);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x01, 8);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x11, 7);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x02, 16);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x08, 12);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x02, 8);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x08, 7);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x03, 16);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x09, 12);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x03, 8);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x09, 7);
|
||||||
|
}
|
||||||
|
// Write output
|
||||||
|
let oi = 0;
|
||||||
|
out[oi++] = (y00 + x00) | 0;
|
||||||
|
out[oi++] = (y01 + x01) | 0;
|
||||||
|
out[oi++] = (y02 + x02) | 0;
|
||||||
|
out[oi++] = (y03 + x03) | 0;
|
||||||
|
out[oi++] = (y04 + x04) | 0;
|
||||||
|
out[oi++] = (y05 + x05) | 0;
|
||||||
|
out[oi++] = (y06 + x06) | 0;
|
||||||
|
out[oi++] = (y07 + x07) | 0;
|
||||||
|
out[oi++] = (y08 + x08) | 0;
|
||||||
|
out[oi++] = (y09 + x09) | 0;
|
||||||
|
out[oi++] = (y10 + x10) | 0;
|
||||||
|
out[oi++] = (y11 + x11) | 0;
|
||||||
|
out[oi++] = (y12 + x12) | 0;
|
||||||
|
out[oi++] = (y13 + x13) | 0;
|
||||||
|
out[oi++] = (y14 + x14) | 0;
|
||||||
|
out[oi++] = (y15 + x15) | 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* hchacha helper method, used primarily in xchacha, to hash
|
||||||
|
* key and nonce into key' and nonce'.
|
||||||
|
* Same as chachaCore, but there doesn't seem to be a way to move the block
|
||||||
|
* out without 25% performance hit.
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
function hchacha(s, k, i, o32) {
|
||||||
|
let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3];
|
||||||
|
for (let r = 0; r < 20; r += 2) {
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x00, 16);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x08, 12);
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x00, 8);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x08, 7);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x01, 16);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x09, 12);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x01, 8);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x09, 7);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x02, 16);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x10, 12);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x02, 8);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x10, 7);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x03, 16);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x11, 12);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x03, 8);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x11, 7);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x00, 16);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x10, 12);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = (0, _arx_js_1.rotl)(x15 ^ x00, 8);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = (0, _arx_js_1.rotl)(x05 ^ x10, 7);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x01, 16);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x11, 12);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = (0, _arx_js_1.rotl)(x12 ^ x01, 8);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = (0, _arx_js_1.rotl)(x06 ^ x11, 7);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x02, 16);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x08, 12);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = (0, _arx_js_1.rotl)(x13 ^ x02, 8);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = (0, _arx_js_1.rotl)(x07 ^ x08, 7);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x03, 16);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x09, 12);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = (0, _arx_js_1.rotl)(x14 ^ x03, 8);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = (0, _arx_js_1.rotl)(x04 ^ x09, 7);
|
||||||
|
}
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x00;
|
||||||
|
o32[oi++] = x01;
|
||||||
|
o32[oi++] = x02;
|
||||||
|
o32[oi++] = x03;
|
||||||
|
o32[oi++] = x12;
|
||||||
|
o32[oi++] = x13;
|
||||||
|
o32[oi++] = x14;
|
||||||
|
o32[oi++] = x15;
|
||||||
|
}
|
||||||
|
exports.hchacha = hchacha;
|
||||||
|
/**
|
||||||
|
* Original, non-RFC chacha20 from DJB. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
exports.chacha20orig = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
allowShortKeys: true,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* ChaCha stream cipher. Conforms to RFC 8439 (IETF, TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
exports.chacha20 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
allowShortKeys: false,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* XChaCha eXtended-nonce ChaCha. 24-byte nonce.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
exports.xchacha20 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
extendNonceFn: hchacha,
|
||||||
|
allowShortKeys: false,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Reduced 8-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
exports.chacha8 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Reduced 12-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
exports.chacha12 = (0, _arx_js_1.createCipher)(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 12,
|
||||||
|
});
|
||||||
|
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
||||||
|
// Pad to digest size with zeros
|
||||||
|
const updatePadded = (h, msg) => {
|
||||||
|
h.update(msg);
|
||||||
|
const left = msg.length % 16;
|
||||||
|
if (left)
|
||||||
|
h.update(ZEROS16.subarray(left));
|
||||||
|
};
|
||||||
|
const ZEROS32 = /* @__PURE__ */ new Uint8Array(32);
|
||||||
|
function computeTag(fn, key, nonce, data, AAD) {
|
||||||
|
const authKey = fn(key, nonce, ZEROS32);
|
||||||
|
const h = _poly1305_js_1.poly1305.create(authKey);
|
||||||
|
if (AAD)
|
||||||
|
updatePadded(h, AAD);
|
||||||
|
updatePadded(h, data);
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = (0, utils_js_1.createView)(num);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||||
|
(0, utils_js_1.setBigUint64)(view, 8, BigInt(data.length), true);
|
||||||
|
h.update(num);
|
||||||
|
const res = h.digest();
|
||||||
|
authKey.fill(0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* AEAD algorithm from RFC 8439.
|
||||||
|
* Salsa20 and chacha (RFC 8439) use poly1305 differently.
|
||||||
|
* We could have composed them similar to:
|
||||||
|
* https://github.com/paulmillr/scure-base/blob/b266c73dde977b1dd7ef40ef7a23cc15aab526b3/index.ts#L250
|
||||||
|
* But it's hard because of authKey:
|
||||||
|
* In salsa20, authKey changes position in salsa stream.
|
||||||
|
* In chacha, authKey can't be computed inside computeTag, it modifies the counter.
|
||||||
|
*/
|
||||||
|
const _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
|
||||||
|
const tagLength = 16;
|
||||||
|
(0, _assert_js_1.bytes)(key, 32);
|
||||||
|
(0, _assert_js_1.bytes)(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, output) => {
|
||||||
|
const plength = plaintext.length;
|
||||||
|
const clength = plength + tagLength;
|
||||||
|
if (output) {
|
||||||
|
(0, _assert_js_1.bytes)(output, clength);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
output = new Uint8Array(clength);
|
||||||
|
}
|
||||||
|
xorStream(key, nonce, plaintext, output, 1);
|
||||||
|
const tag = computeTag(xorStream, key, nonce, output.subarray(0, -tagLength), AAD);
|
||||||
|
output.set(tag, plength); // append tag
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, output) => {
|
||||||
|
const clength = ciphertext.length;
|
||||||
|
const plength = clength - tagLength;
|
||||||
|
if (clength < tagLength)
|
||||||
|
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||||
|
if (output) {
|
||||||
|
(0, _assert_js_1.bytes)(output, plength);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
output = new Uint8Array(plength);
|
||||||
|
}
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const tag = computeTag(xorStream, key, nonce, data, AAD);
|
||||||
|
if (!(0, utils_js_1.equalBytes)(passedTag, tag))
|
||||||
|
throw new Error('invalid tag');
|
||||||
|
xorStream(key, nonce, data, output, 1);
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
exports._poly1305_aead = _poly1305_aead;
|
||||||
|
/**
|
||||||
|
* ChaCha20-Poly1305 from RFC 8439.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
exports.chacha20poly1305 = (0, utils_js_1.wrapCipher)({ blockSize: 64, nonceLength: 12, tagLength: 16 }, (0, exports._poly1305_aead)(exports.chacha20));
|
||||||
|
/**
|
||||||
|
* XChaCha20-Poly1305 extended-nonce chacha.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
exports.xchacha20poly1305 = (0, utils_js_1.wrapCipher)({ blockSize: 64, nonceLength: 24, tagLength: 16 }, (0, exports._poly1305_aead)(exports.xchacha20));
|
||||||
|
//# sourceMappingURL=chacha.js.map
|
||||||
1
node_modules/@noble/ciphers/chacha.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/chacha.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
node_modules/@noble/ciphers/crypto.d.ts
generated
vendored
Normal file
3
node_modules/@noble/ciphers/crypto.d.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export declare function randomBytes(bytesLength?: number): Uint8Array;
|
||||||
|
export declare function getWebcryptoSubtle(): any;
|
||||||
|
//# sourceMappingURL=crypto.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/crypto.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/crypto.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["src/crypto.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,WAAW,SAAK,GAAG,UAAU,CAIxD;AAED,wBAAgB,kBAAkB,QAGjC"}
|
||||||
17
node_modules/@noble/ciphers/crypto.js
generated
vendored
Normal file
17
node_modules/@noble/ciphers/crypto.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.getWebcryptoSubtle = exports.randomBytes = void 0;
|
||||||
|
const cr = typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
|
||||||
|
function randomBytes(bytesLength = 32) {
|
||||||
|
if (cr && typeof cr.getRandomValues === 'function')
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
}
|
||||||
|
exports.randomBytes = randomBytes;
|
||||||
|
function getWebcryptoSubtle() {
|
||||||
|
if (cr && typeof cr.subtle === 'object' && cr.subtle != null)
|
||||||
|
return cr.subtle;
|
||||||
|
throw new Error('crypto.subtle must be defined');
|
||||||
|
}
|
||||||
|
exports.getWebcryptoSubtle = getWebcryptoSubtle;
|
||||||
|
//# sourceMappingURL=crypto.js.map
|
||||||
1
node_modules/@noble/ciphers/crypto.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/crypto.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["src/crypto.ts"],"names":[],"mappings":";;;AAGA,MAAM,EAAE,GAAG,OAAO,UAAU,KAAK,QAAQ,IAAI,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAEpG,SAAgB,WAAW,CAAC,WAAW,GAAG,EAAE;IAC1C,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,eAAe,KAAK,UAAU;QAChD,OAAO,EAAE,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;AAC5D,CAAC;AAJD,kCAIC;AAED,SAAgB,kBAAkB;IAChC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC,MAAM,CAAC;IAC/E,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;AACnD,CAAC;AAHD,gDAGC"}
|
||||||
3
node_modules/@noble/ciphers/cryptoNode.d.ts
generated
vendored
Normal file
3
node_modules/@noble/ciphers/cryptoNode.d.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export declare function randomBytes(bytesLength?: number): Uint8Array;
|
||||||
|
export declare function getWebcryptoSubtle(): any;
|
||||||
|
//# sourceMappingURL=cryptoNode.d.ts.map
|
||||||
1
node_modules/@noble/ciphers/cryptoNode.d.ts.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/cryptoNode.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"cryptoNode.d.ts","sourceRoot":"","sources":["src/cryptoNode.ts"],"names":[],"mappings":"AAOA,wBAAgB,WAAW,CAAC,WAAW,SAAK,GAAG,UAAU,CAIxD;AAED,wBAAgB,kBAAkB,QAGjC"}
|
||||||
22
node_modules/@noble/ciphers/cryptoNode.js
generated
vendored
Normal file
22
node_modules/@noble/ciphers/cryptoNode.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.getWebcryptoSubtle = exports.randomBytes = void 0;
|
||||||
|
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
||||||
|
// See utils.ts for details.
|
||||||
|
// The file will throw on node.js 14 and earlier.
|
||||||
|
// @ts-ignore
|
||||||
|
const nc = require("node:crypto");
|
||||||
|
const cr = nc && typeof nc === 'object' && 'webcrypto' in nc ? nc.webcrypto : undefined;
|
||||||
|
function randomBytes(bytesLength = 32) {
|
||||||
|
if (cr && typeof cr.getRandomValues === 'function')
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
}
|
||||||
|
exports.randomBytes = randomBytes;
|
||||||
|
function getWebcryptoSubtle() {
|
||||||
|
if (cr && typeof cr.subtle === 'object' && cr.subtle != null)
|
||||||
|
return cr.subtle;
|
||||||
|
throw new Error('crypto.subtle must be defined');
|
||||||
|
}
|
||||||
|
exports.getWebcryptoSubtle = getWebcryptoSubtle;
|
||||||
|
//# sourceMappingURL=cryptoNode.js.map
|
||||||
1
node_modules/@noble/ciphers/cryptoNode.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/cryptoNode.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"cryptoNode.js","sourceRoot":"","sources":["src/cryptoNode.ts"],"names":[],"mappings":";;;AAAA,oFAAoF;AACpF,4BAA4B;AAC5B,iDAAiD;AACjD,aAAa;AACb,kCAAkC;AAClC,MAAM,EAAE,GAAG,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,WAAW,IAAI,EAAE,CAAC,CAAC,CAAE,EAAE,CAAC,SAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;AAEjG,SAAgB,WAAW,CAAC,WAAW,GAAG,EAAE;IAC1C,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,eAAe,KAAK,UAAU;QAChD,OAAO,EAAE,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;AAC5D,CAAC;AAJD,kCAIC;AAED,SAAgB,kBAAkB;IAChC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC,MAAM,CAAC;IAC/E,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;AACnD,CAAC;AAHD,gDAGC"}
|
||||||
170
node_modules/@noble/ciphers/esm/_arx.js
generated
vendored
Normal file
170
node_modules/@noble/ciphers/esm/_arx.js
generated
vendored
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers.
|
||||||
|
import { number as anumber, bytes as abytes, bool as abool } from './_assert.js';
|
||||||
|
import { checkOpts, u32 } from './utils.js';
|
||||||
|
/*
|
||||||
|
RFC8439 requires multi-step cipher stream, where
|
||||||
|
authKey starts with counter: 0, actual msg with counter: 1.
|
||||||
|
|
||||||
|
For this, we need a way to re-use nonce / counter:
|
||||||
|
|
||||||
|
const counter = new Uint8Array(4);
|
||||||
|
chacha(..., counter, ...); // counter is now 1
|
||||||
|
chacha(..., counter, ...); // counter is now 2
|
||||||
|
|
||||||
|
This is complicated:
|
||||||
|
|
||||||
|
- 32-bit counters are enough, no need for 64-bit: max ArrayBuffer size in JS is 4GB
|
||||||
|
- Original papers don't allow mutating counters
|
||||||
|
- Counter overflow is undefined [^1]
|
||||||
|
- Idea A: allow providing (nonce | counter) instead of just nonce, re-use it
|
||||||
|
- Caveat: Cannot be re-used through all cases:
|
||||||
|
- * chacha has (counter | nonce)
|
||||||
|
- * xchacha has (nonce16 | counter | nonce16)
|
||||||
|
- Idea B: separate nonce / counter and provide separate API for counter re-use
|
||||||
|
- Caveat: there are different counter sizes depending on an algorithm.
|
||||||
|
- salsa & chacha also differ in structures of key & sigma:
|
||||||
|
salsa20: s[0] | k(4) | s[1] | nonce(2) | ctr(2) | s[2] | k(4) | s[3]
|
||||||
|
chacha: s(4) | k(8) | ctr(1) | nonce(3)
|
||||||
|
chacha20orig: s(4) | k(8) | ctr(2) | nonce(2)
|
||||||
|
- Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)`
|
||||||
|
- Caveat: we can't re-use counter array
|
||||||
|
|
||||||
|
xchacha [^2] uses the subkey and remaining 8 byte nonce with ChaCha20 as normal
|
||||||
|
(prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).
|
||||||
|
|
||||||
|
[^1]: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/
|
||||||
|
[^2]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2
|
||||||
|
*/
|
||||||
|
// We can't make top-level var depend on utils.utf8ToBytes
|
||||||
|
// because it's not present in all envs. Creating a similar fn here
|
||||||
|
const _utf8ToBytes = (str) => Uint8Array.from(str.split('').map((c) => c.charCodeAt(0)));
|
||||||
|
const sigma16 = _utf8ToBytes('expand 16-byte k');
|
||||||
|
const sigma32 = _utf8ToBytes('expand 32-byte k');
|
||||||
|
const sigma16_32 = u32(sigma16);
|
||||||
|
const sigma32_32 = u32(sigma32);
|
||||||
|
export const sigma = sigma32_32.slice();
|
||||||
|
export function rotl(a, b) {
|
||||||
|
return (a << b) | (a >>> (32 - b));
|
||||||
|
}
|
||||||
|
// Is byte array aligned to 4 byte offset (u32)?
|
||||||
|
function isAligned32(b) {
|
||||||
|
return b.byteOffset % 4 === 0;
|
||||||
|
}
|
||||||
|
// Salsa and Chacha block length is always 512-bit
|
||||||
|
const BLOCK_LEN = 64;
|
||||||
|
const BLOCK_LEN32 = 16;
|
||||||
|
// new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ]
|
||||||
|
// new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ]
|
||||||
|
const MAX_COUNTER = 2 ** 32 - 1;
|
||||||
|
const U32_EMPTY = new Uint32Array();
|
||||||
|
function runCipher(core, sigma, key, nonce, data, output, counter, rounds) {
|
||||||
|
const len = data.length;
|
||||||
|
const block = new Uint8Array(BLOCK_LEN);
|
||||||
|
const b32 = u32(block);
|
||||||
|
// Make sure that buffers aligned to 4 bytes
|
||||||
|
const isAligned = isAligned32(data) && isAligned32(output);
|
||||||
|
const d32 = isAligned ? u32(data) : U32_EMPTY;
|
||||||
|
const o32 = isAligned ? u32(output) : U32_EMPTY;
|
||||||
|
for (let pos = 0; pos < len; counter++) {
|
||||||
|
core(sigma, key, nonce, b32, counter, rounds);
|
||||||
|
if (counter >= MAX_COUNTER)
|
||||||
|
throw new Error('arx: counter overflow');
|
||||||
|
const take = Math.min(BLOCK_LEN, len - pos);
|
||||||
|
// aligned to 4 bytes
|
||||||
|
if (isAligned && take === BLOCK_LEN) {
|
||||||
|
const pos32 = pos / 4;
|
||||||
|
if (pos % 4 !== 0)
|
||||||
|
throw new Error('arx: invalid block position');
|
||||||
|
for (let j = 0, posj; j < BLOCK_LEN32; j++) {
|
||||||
|
posj = pos32 + j;
|
||||||
|
o32[posj] = d32[posj] ^ b32[j];
|
||||||
|
}
|
||||||
|
pos += BLOCK_LEN;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let j = 0, posj; j < take; j++) {
|
||||||
|
posj = pos + j;
|
||||||
|
output[posj] = data[posj] ^ block[j];
|
||||||
|
}
|
||||||
|
pos += take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function createCipher(core, opts) {
|
||||||
|
const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts);
|
||||||
|
if (typeof core !== 'function')
|
||||||
|
throw new Error('core must be a function');
|
||||||
|
anumber(counterLength);
|
||||||
|
anumber(rounds);
|
||||||
|
abool(counterRight);
|
||||||
|
abool(allowShortKeys);
|
||||||
|
return (key, nonce, data, output, counter = 0) => {
|
||||||
|
abytes(key);
|
||||||
|
abytes(nonce);
|
||||||
|
abytes(data);
|
||||||
|
const len = data.length;
|
||||||
|
if (!output)
|
||||||
|
output = new Uint8Array(len);
|
||||||
|
abytes(output);
|
||||||
|
anumber(counter);
|
||||||
|
if (counter < 0 || counter >= MAX_COUNTER)
|
||||||
|
throw new Error('arx: counter overflow');
|
||||||
|
if (output.length < len)
|
||||||
|
throw new Error(`arx: output (${output.length}) is shorter than data (${len})`);
|
||||||
|
const toClean = [];
|
||||||
|
// Key & sigma
|
||||||
|
// key=16 -> sigma16, k=key|key
|
||||||
|
// key=32 -> sigma32, k=key
|
||||||
|
let l = key.length, k, sigma;
|
||||||
|
if (l === 32) {
|
||||||
|
k = key.slice();
|
||||||
|
toClean.push(k);
|
||||||
|
sigma = sigma32_32;
|
||||||
|
}
|
||||||
|
else if (l === 16 && allowShortKeys) {
|
||||||
|
k = new Uint8Array(32);
|
||||||
|
k.set(key);
|
||||||
|
k.set(key, 16);
|
||||||
|
sigma = sigma16_32;
|
||||||
|
toClean.push(k);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`arx: invalid 32-byte key, got length=${l}`);
|
||||||
|
}
|
||||||
|
// Nonce
|
||||||
|
// salsa20: 8 (8-byte counter)
|
||||||
|
// chacha20orig: 8 (8-byte counter)
|
||||||
|
// chacha20: 12 (4-byte counter)
|
||||||
|
// xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce)
|
||||||
|
// xchacha20: 24 (16 -> hchacha, 8 -> old nonce)
|
||||||
|
// Align nonce to 4 bytes
|
||||||
|
if (!isAligned32(nonce)) {
|
||||||
|
nonce = nonce.slice();
|
||||||
|
toClean.push(nonce);
|
||||||
|
}
|
||||||
|
const k32 = u32(k);
|
||||||
|
// hsalsa & hchacha: handle extended nonce
|
||||||
|
if (extendNonceFn) {
|
||||||
|
if (nonce.length !== 24)
|
||||||
|
throw new Error(`arx: extended nonce must be 24 bytes`);
|
||||||
|
extendNonceFn(sigma, k32, u32(nonce.subarray(0, 16)), k32);
|
||||||
|
nonce = nonce.subarray(16);
|
||||||
|
}
|
||||||
|
// Handle nonce counter
|
||||||
|
const nonceNcLen = 16 - counterLength;
|
||||||
|
if (nonceNcLen !== nonce.length)
|
||||||
|
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
|
||||||
|
// Pad counter when nonce is 64 bit
|
||||||
|
if (nonceNcLen !== 12) {
|
||||||
|
const nc = new Uint8Array(12);
|
||||||
|
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
|
||||||
|
nonce = nc;
|
||||||
|
toClean.push(nonce);
|
||||||
|
}
|
||||||
|
const n32 = u32(nonce);
|
||||||
|
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
|
||||||
|
while (toClean.length > 0)
|
||||||
|
toClean.pop().fill(0);
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=_arx.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/_arx.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/_arx.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
41
node_modules/@noble/ciphers/esm/_assert.js
generated
vendored
Normal file
41
node_modules/@noble/ciphers/esm/_assert.js
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
function number(n) {
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0)
|
||||||
|
throw new Error(`positive integer expected, not ${n}`);
|
||||||
|
}
|
||||||
|
function bool(b) {
|
||||||
|
if (typeof b !== 'boolean')
|
||||||
|
throw new Error(`boolean expected, not ${b}`);
|
||||||
|
}
|
||||||
|
export function isBytes(a) {
|
||||||
|
return (a instanceof Uint8Array ||
|
||||||
|
(a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array'));
|
||||||
|
}
|
||||||
|
function bytes(b, ...lengths) {
|
||||||
|
if (!isBytes(b))
|
||||||
|
throw new Error('Uint8Array expected');
|
||||||
|
if (lengths.length > 0 && !lengths.includes(b.length))
|
||||||
|
throw new Error(`Uint8Array expected of length ${lengths}, not of length=${b.length}`);
|
||||||
|
}
|
||||||
|
function hash(hash) {
|
||||||
|
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
||||||
|
throw new Error('hash must be wrapped by utils.wrapConstructor');
|
||||||
|
number(hash.outputLen);
|
||||||
|
number(hash.blockLen);
|
||||||
|
}
|
||||||
|
function exists(instance, checkFinished = true) {
|
||||||
|
if (instance.destroyed)
|
||||||
|
throw new Error('Hash instance has been destroyed');
|
||||||
|
if (checkFinished && instance.finished)
|
||||||
|
throw new Error('Hash#digest() has already been called');
|
||||||
|
}
|
||||||
|
function output(out, instance) {
|
||||||
|
bytes(out);
|
||||||
|
const min = instance.outputLen;
|
||||||
|
if (out.length < min) {
|
||||||
|
throw new Error(`digestInto() expects output buffer of length at least ${min}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { number, bool, bytes, hash, exists, output };
|
||||||
|
const assert = { number, bool, bytes, hash, exists, output };
|
||||||
|
export default assert;
|
||||||
|
//# sourceMappingURL=_assert.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/_assert.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/_assert.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"_assert.js","sourceRoot":"","sources":["../src/_assert.ts"],"names":[],"mappings":"AAAA,SAAS,MAAM,CAAC,CAAS;IACvB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,EAAE,CAAC,CAAC;AAChG,CAAC;AAED,SAAS,IAAI,CAAC,CAAU;IACtB,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,CAAU;IAChC,OAAO,CACL,CAAC,YAAY,UAAU;QACvB,CAAC,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,KAAK,YAAY,CAAC,CAC5E,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,CAAyB,EAAE,GAAG,OAAiB;IAC5D,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,mBAAmB,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AAC3F,CAAC;AAQD,SAAS,IAAI,CAAC,IAAU;IACtB,IAAI,OAAO,IAAI,KAAK,UAAU,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU;QACjE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,MAAM,CAAC,QAAa,EAAE,aAAa,GAAG,IAAI;IACjD,IAAI,QAAQ,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC5E,IAAI,aAAa,IAAI,QAAQ,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;AACnG,CAAC;AAED,SAAS,MAAM,CAAC,GAAQ,EAAE,QAAa;IACrC,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC;IAC/B,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,yDAAyD,GAAG,EAAE,CAAC,CAAC;IAClF,CAAC;AACH,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACrD,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC7D,eAAe,MAAM,CAAC"}
|
||||||
287
node_modules/@noble/ciphers/esm/_micro.js
generated
vendored
Normal file
287
node_modules/@noble/ciphers/esm/_micro.js
generated
vendored
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||||
|
// prettier-ignore
|
||||||
|
import { createView, setBigUint64, wrapCipher, bytesToHex, concatBytes, equalBytes, hexToNumber, numberToBytesBE, } from './utils.js';
|
||||||
|
import { createCipher, rotl } from './_arx.js';
|
||||||
|
import { bytes as abytes } from './_assert.js';
|
||||||
|
/*
|
||||||
|
noble-ciphers-micro: more auditable, but slower version of salsa20, chacha & poly1305.
|
||||||
|
Implements the same algorithms that are present in other files, but without
|
||||||
|
unrolled loops (https://en.wikipedia.org/wiki/Loop_unrolling).
|
||||||
|
*/
|
||||||
|
function bytesToNumberLE(bytes) {
|
||||||
|
return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
|
||||||
|
}
|
||||||
|
function numberToBytesLE(n, len) {
|
||||||
|
return numberToBytesBE(n, len).reverse();
|
||||||
|
}
|
||||||
|
function salsaQR(x, a, b, c, d) {
|
||||||
|
x[b] ^= rotl((x[a] + x[d]) | 0, 7);
|
||||||
|
x[c] ^= rotl((x[b] + x[a]) | 0, 9);
|
||||||
|
x[d] ^= rotl((x[c] + x[b]) | 0, 13);
|
||||||
|
x[a] ^= rotl((x[d] + x[c]) | 0, 18);
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
function chachaQR(x, a, b, c, d) {
|
||||||
|
x[a] = (x[a] + x[b]) | 0;
|
||||||
|
x[d] = rotl(x[d] ^ x[a], 16);
|
||||||
|
x[c] = (x[c] + x[d]) | 0;
|
||||||
|
x[b] = rotl(x[b] ^ x[c], 12);
|
||||||
|
x[a] = (x[a] + x[b]) | 0;
|
||||||
|
x[d] = rotl(x[d] ^ x[a], 8);
|
||||||
|
x[c] = (x[c] + x[d]) | 0;
|
||||||
|
x[b] = rotl(x[b] ^ x[c], 7);
|
||||||
|
}
|
||||||
|
function salsaRound(x, rounds = 20) {
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
salsaQR(x, 0, 4, 8, 12);
|
||||||
|
salsaQR(x, 5, 9, 13, 1);
|
||||||
|
salsaQR(x, 10, 14, 2, 6);
|
||||||
|
salsaQR(x, 15, 3, 7, 11);
|
||||||
|
salsaQR(x, 0, 1, 2, 3);
|
||||||
|
salsaQR(x, 5, 6, 7, 4);
|
||||||
|
salsaQR(x, 10, 11, 8, 9);
|
||||||
|
salsaQR(x, 15, 12, 13, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function chachaRound(x, rounds = 20) {
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
chachaQR(x, 0, 4, 8, 12);
|
||||||
|
chachaQR(x, 1, 5, 9, 13);
|
||||||
|
chachaQR(x, 2, 6, 10, 14);
|
||||||
|
chachaQR(x, 3, 7, 11, 15);
|
||||||
|
chachaQR(x, 0, 5, 10, 15);
|
||||||
|
chachaQR(x, 1, 6, 11, 12);
|
||||||
|
chachaQR(x, 2, 7, 8, 13);
|
||||||
|
chachaQR(x, 3, 4, 9, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function salsaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
// prettier-ignore
|
||||||
|
const y = new Uint32Array([
|
||||||
|
s[0], k[0], k[1], k[2], // "expa" Key Key Key
|
||||||
|
k[3], s[1], n[0], n[1], // Key "nd 3" Nonce Nonce
|
||||||
|
cnt, 0, s[2], k[4], // Pos. Pos. "2-by" Key
|
||||||
|
k[5], k[6], k[7], s[3], // Key Key Key "te k"
|
||||||
|
]);
|
||||||
|
const x = y.slice();
|
||||||
|
salsaRound(x, rounds);
|
||||||
|
for (let i = 0; i < 16; i++)
|
||||||
|
out[i] = (y[i] + x[i]) | 0;
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
export function hsalsa(s, k, i, o32) {
|
||||||
|
const x = new Uint32Array([
|
||||||
|
s[0], k[0], k[1], k[2],
|
||||||
|
k[3], s[1], i[0], i[1],
|
||||||
|
i[2], i[3], s[2], k[4],
|
||||||
|
k[5], k[6], k[7], s[3]
|
||||||
|
]);
|
||||||
|
salsaRound(x, 20);
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x[0];
|
||||||
|
o32[oi++] = x[5];
|
||||||
|
o32[oi++] = x[10];
|
||||||
|
o32[oi++] = x[15];
|
||||||
|
o32[oi++] = x[6];
|
||||||
|
o32[oi++] = x[7];
|
||||||
|
o32[oi++] = x[8];
|
||||||
|
o32[oi++] = x[9];
|
||||||
|
}
|
||||||
|
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
// prettier-ignore
|
||||||
|
const y = new Uint32Array([
|
||||||
|
s[0], s[1], s[2], s[3], // "expa" "nd 3" "2-by" "te k"
|
||||||
|
k[0], k[1], k[2], k[3], // Key Key Key Key
|
||||||
|
k[4], k[5], k[6], k[7], // Key Key Key Key
|
||||||
|
cnt, n[0], n[1], n[2], // Counter Counter Nonce Nonce
|
||||||
|
]);
|
||||||
|
const x = y.slice();
|
||||||
|
chachaRound(x, rounds);
|
||||||
|
for (let i = 0; i < 16; i++)
|
||||||
|
out[i] = (y[i] + x[i]) | 0;
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
export function hchacha(s, k, i, o32) {
|
||||||
|
const x = new Uint32Array([
|
||||||
|
s[0], s[1], s[2], s[3],
|
||||||
|
k[0], k[1], k[2], k[3],
|
||||||
|
k[4], k[5], k[6], k[7],
|
||||||
|
i[0], i[1], i[2], i[3],
|
||||||
|
]);
|
||||||
|
chachaRound(x, 20);
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x[0];
|
||||||
|
o32[oi++] = x[1];
|
||||||
|
o32[oi++] = x[2];
|
||||||
|
o32[oi++] = x[3];
|
||||||
|
o32[oi++] = x[12];
|
||||||
|
o32[oi++] = x[13];
|
||||||
|
o32[oi++] = x[14];
|
||||||
|
o32[oi++] = x[15];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* salsa20, 12-byte nonce.
|
||||||
|
*/
|
||||||
|
export const salsa20 = /* @__PURE__ */ createCipher(salsaCore, {
|
||||||
|
allowShortKeys: true,
|
||||||
|
counterRight: true,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* xsalsa20, 24-byte nonce.
|
||||||
|
*/
|
||||||
|
export const xsalsa20 = /* @__PURE__ */ createCipher(salsaCore, {
|
||||||
|
counterRight: true,
|
||||||
|
extendNonceFn: hsalsa,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* chacha20 non-RFC, original version by djb. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
export const chacha20orig = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
allowShortKeys: true,
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* chacha20 RFC 8439 (IETF / TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
*/
|
||||||
|
export const chacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* xchacha20 eXtended-nonce. https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
export const xchacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
extendNonceFn: hchacha,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 8-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
export const chacha8 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 12-round chacha from the original paper.
|
||||||
|
*/
|
||||||
|
export const chacha12 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 12,
|
||||||
|
});
|
||||||
|
const POW_2_130_5 = BigInt(2) ** BigInt(130) - BigInt(5);
|
||||||
|
const POW_2_128_1 = BigInt(2) ** BigInt(16 * 8) - BigInt(1);
|
||||||
|
const CLAMP_R = BigInt('0x0ffffffc0ffffffc0ffffffc0fffffff');
|
||||||
|
const _0 = BigInt(0);
|
||||||
|
const _1 = BigInt(1);
|
||||||
|
// Can be speed-up using BigUint64Array, but would be more complicated
|
||||||
|
export function poly1305(msg, key) {
|
||||||
|
abytes(msg);
|
||||||
|
abytes(key);
|
||||||
|
let acc = _0;
|
||||||
|
const r = bytesToNumberLE(key.subarray(0, 16)) & CLAMP_R;
|
||||||
|
const s = bytesToNumberLE(key.subarray(16));
|
||||||
|
// Process by 16 byte chunks
|
||||||
|
for (let i = 0; i < msg.length; i += 16) {
|
||||||
|
const m = msg.subarray(i, i + 16);
|
||||||
|
const n = bytesToNumberLE(m) | (_1 << BigInt(8 * m.length));
|
||||||
|
acc = ((acc + n) * r) % POW_2_130_5;
|
||||||
|
}
|
||||||
|
const res = (acc + s) & POW_2_128_1;
|
||||||
|
return numberToBytesLE(res, 16);
|
||||||
|
}
|
||||||
|
function computeTag(fn, key, nonce, ciphertext, AAD) {
|
||||||
|
const res = [];
|
||||||
|
if (AAD) {
|
||||||
|
res.push(AAD);
|
||||||
|
const leftover = AAD.length % 16;
|
||||||
|
if (leftover > 0)
|
||||||
|
res.push(new Uint8Array(16 - leftover));
|
||||||
|
}
|
||||||
|
res.push(ciphertext);
|
||||||
|
const leftover = ciphertext.length % 16;
|
||||||
|
if (leftover > 0)
|
||||||
|
res.push(new Uint8Array(16 - leftover));
|
||||||
|
// Lengths
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = createView(num);
|
||||||
|
setBigUint64(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||||
|
setBigUint64(view, 8, BigInt(ciphertext.length), true);
|
||||||
|
res.push(num);
|
||||||
|
const authKey = fn(key, nonce, new Uint8Array(32));
|
||||||
|
return poly1305(concatBytes(...res), authKey);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa.
|
||||||
|
*/
|
||||||
|
export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 24, tagLength: 16 }, function xsalsa20poly1305(key, nonce) {
|
||||||
|
abytes(key);
|
||||||
|
abytes(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
abytes(plaintext);
|
||||||
|
const m = concatBytes(new Uint8Array(32), plaintext);
|
||||||
|
const c = xsalsa20(key, nonce, m);
|
||||||
|
const authKey = c.subarray(0, 32);
|
||||||
|
const data = c.subarray(32);
|
||||||
|
const tag = poly1305(data, authKey);
|
||||||
|
return concatBytes(tag, data);
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
abytes(ciphertext);
|
||||||
|
if (ciphertext.length < 16)
|
||||||
|
throw new Error('encrypted data must be at least 16 bytes');
|
||||||
|
const c = concatBytes(new Uint8Array(16), ciphertext);
|
||||||
|
const authKey = xsalsa20(key, nonce, new Uint8Array(32));
|
||||||
|
const tag = poly1305(c.subarray(32), authKey);
|
||||||
|
if (!equalBytes(c.subarray(16, 32), tag))
|
||||||
|
throw new Error('invalid poly1305 tag');
|
||||||
|
return xsalsa20(key, nonce, c).subarray(32);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Alias to xsalsa20-poly1305
|
||||||
|
*/
|
||||||
|
export function secretbox(key, nonce) {
|
||||||
|
const xs = xsalsa20poly1305(key, nonce);
|
||||||
|
return { seal: xs.encrypt, open: xs.decrypt };
|
||||||
|
}
|
||||||
|
export const _poly1305_aead = (fn) => (key, nonce, AAD) => {
|
||||||
|
const tagLength = 16;
|
||||||
|
const keyLength = 32;
|
||||||
|
abytes(key, keyLength);
|
||||||
|
abytes(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
abytes(plaintext);
|
||||||
|
const res = fn(key, nonce, plaintext, undefined, 1);
|
||||||
|
const tag = computeTag(fn, key, nonce, res, AAD);
|
||||||
|
return concatBytes(res, tag);
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
abytes(ciphertext);
|
||||||
|
if (ciphertext.length < tagLength)
|
||||||
|
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const tag = computeTag(fn, key, nonce, data, AAD);
|
||||||
|
if (!equalBytes(passedTag, tag))
|
||||||
|
throw new Error('invalid poly1305 tag');
|
||||||
|
return fn(key, nonce, data, undefined, 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* chacha20-poly1305 12-byte-nonce chacha.
|
||||||
|
*/
|
||||||
|
export const chacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20));
|
||||||
|
/**
|
||||||
|
* xchacha20-poly1305 eXtended-nonce (24 bytes) chacha.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
export const xchacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 24, tagLength: 16 }, _poly1305_aead(xchacha20));
|
||||||
|
//# sourceMappingURL=_micro.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/_micro.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/_micro.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
264
node_modules/@noble/ciphers/esm/_poly1305.js
generated
vendored
Normal file
264
node_modules/@noble/ciphers/esm/_poly1305.js
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import { exists as aexists, bytes as abytes, output as aoutput } from './_assert.js';
|
||||||
|
import { toBytes } from './utils.js';
|
||||||
|
// Poly1305 is a fast and parallel secret-key message-authentication code.
|
||||||
|
// https://cr.yp.to/mac.html, https://cr.yp.to/mac/poly1305-20050329.pdf
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8439
|
||||||
|
// Based on Public Domain poly1305-donna https://github.com/floodyberry/poly1305-donna
|
||||||
|
const u8to16 = (a, i) => (a[i++] & 0xff) | ((a[i++] & 0xff) << 8);
|
||||||
|
class Poly1305 {
|
||||||
|
constructor(key) {
|
||||||
|
this.blockLen = 16;
|
||||||
|
this.outputLen = 16;
|
||||||
|
this.buffer = new Uint8Array(16);
|
||||||
|
this.r = new Uint16Array(10);
|
||||||
|
this.h = new Uint16Array(10);
|
||||||
|
this.pad = new Uint16Array(8);
|
||||||
|
this.pos = 0;
|
||||||
|
this.finished = false;
|
||||||
|
key = toBytes(key);
|
||||||
|
abytes(key, 32);
|
||||||
|
const t0 = u8to16(key, 0);
|
||||||
|
const t1 = u8to16(key, 2);
|
||||||
|
const t2 = u8to16(key, 4);
|
||||||
|
const t3 = u8to16(key, 6);
|
||||||
|
const t4 = u8to16(key, 8);
|
||||||
|
const t5 = u8to16(key, 10);
|
||||||
|
const t6 = u8to16(key, 12);
|
||||||
|
const t7 = u8to16(key, 14);
|
||||||
|
// https://github.com/floodyberry/poly1305-donna/blob/e6ad6e091d30d7f4ec2d4f978be1fcfcbce72781/poly1305-donna-16.h#L47
|
||||||
|
this.r[0] = t0 & 0x1fff;
|
||||||
|
this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff;
|
||||||
|
this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03;
|
||||||
|
this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff;
|
||||||
|
this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff;
|
||||||
|
this.r[5] = (t4 >>> 1) & 0x1ffe;
|
||||||
|
this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff;
|
||||||
|
this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81;
|
||||||
|
this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff;
|
||||||
|
this.r[9] = (t7 >>> 5) & 0x007f;
|
||||||
|
for (let i = 0; i < 8; i++)
|
||||||
|
this.pad[i] = u8to16(key, 16 + 2 * i);
|
||||||
|
}
|
||||||
|
process(data, offset, isLast = false) {
|
||||||
|
const hibit = isLast ? 0 : 1 << 11;
|
||||||
|
const { h, r } = this;
|
||||||
|
const r0 = r[0];
|
||||||
|
const r1 = r[1];
|
||||||
|
const r2 = r[2];
|
||||||
|
const r3 = r[3];
|
||||||
|
const r4 = r[4];
|
||||||
|
const r5 = r[5];
|
||||||
|
const r6 = r[6];
|
||||||
|
const r7 = r[7];
|
||||||
|
const r8 = r[8];
|
||||||
|
const r9 = r[9];
|
||||||
|
const t0 = u8to16(data, offset + 0);
|
||||||
|
const t1 = u8to16(data, offset + 2);
|
||||||
|
const t2 = u8to16(data, offset + 4);
|
||||||
|
const t3 = u8to16(data, offset + 6);
|
||||||
|
const t4 = u8to16(data, offset + 8);
|
||||||
|
const t5 = u8to16(data, offset + 10);
|
||||||
|
const t6 = u8to16(data, offset + 12);
|
||||||
|
const t7 = u8to16(data, offset + 14);
|
||||||
|
let h0 = h[0] + (t0 & 0x1fff);
|
||||||
|
let h1 = h[1] + (((t0 >>> 13) | (t1 << 3)) & 0x1fff);
|
||||||
|
let h2 = h[2] + (((t1 >>> 10) | (t2 << 6)) & 0x1fff);
|
||||||
|
let h3 = h[3] + (((t2 >>> 7) | (t3 << 9)) & 0x1fff);
|
||||||
|
let h4 = h[4] + (((t3 >>> 4) | (t4 << 12)) & 0x1fff);
|
||||||
|
let h5 = h[5] + ((t4 >>> 1) & 0x1fff);
|
||||||
|
let h6 = h[6] + (((t4 >>> 14) | (t5 << 2)) & 0x1fff);
|
||||||
|
let h7 = h[7] + (((t5 >>> 11) | (t6 << 5)) & 0x1fff);
|
||||||
|
let h8 = h[8] + (((t6 >>> 8) | (t7 << 8)) & 0x1fff);
|
||||||
|
let h9 = h[9] + ((t7 >>> 5) | hibit);
|
||||||
|
let c = 0;
|
||||||
|
let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6);
|
||||||
|
c = d0 >>> 13;
|
||||||
|
d0 &= 0x1fff;
|
||||||
|
d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1);
|
||||||
|
c += d0 >>> 13;
|
||||||
|
d0 &= 0x1fff;
|
||||||
|
let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7);
|
||||||
|
c = d1 >>> 13;
|
||||||
|
d1 &= 0x1fff;
|
||||||
|
d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2);
|
||||||
|
c += d1 >>> 13;
|
||||||
|
d1 &= 0x1fff;
|
||||||
|
let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8);
|
||||||
|
c = d2 >>> 13;
|
||||||
|
d2 &= 0x1fff;
|
||||||
|
d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3);
|
||||||
|
c += d2 >>> 13;
|
||||||
|
d2 &= 0x1fff;
|
||||||
|
let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9);
|
||||||
|
c = d3 >>> 13;
|
||||||
|
d3 &= 0x1fff;
|
||||||
|
d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4);
|
||||||
|
c += d3 >>> 13;
|
||||||
|
d3 &= 0x1fff;
|
||||||
|
let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
|
||||||
|
c = d4 >>> 13;
|
||||||
|
d4 &= 0x1fff;
|
||||||
|
d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5);
|
||||||
|
c += d4 >>> 13;
|
||||||
|
d4 &= 0x1fff;
|
||||||
|
let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1;
|
||||||
|
c = d5 >>> 13;
|
||||||
|
d5 &= 0x1fff;
|
||||||
|
d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6);
|
||||||
|
c += d5 >>> 13;
|
||||||
|
d5 &= 0x1fff;
|
||||||
|
let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2;
|
||||||
|
c = d6 >>> 13;
|
||||||
|
d6 &= 0x1fff;
|
||||||
|
d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7);
|
||||||
|
c += d6 >>> 13;
|
||||||
|
d6 &= 0x1fff;
|
||||||
|
let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3;
|
||||||
|
c = d7 >>> 13;
|
||||||
|
d7 &= 0x1fff;
|
||||||
|
d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8);
|
||||||
|
c += d7 >>> 13;
|
||||||
|
d7 &= 0x1fff;
|
||||||
|
let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4;
|
||||||
|
c = d8 >>> 13;
|
||||||
|
d8 &= 0x1fff;
|
||||||
|
d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9);
|
||||||
|
c += d8 >>> 13;
|
||||||
|
d8 &= 0x1fff;
|
||||||
|
let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5;
|
||||||
|
c = d9 >>> 13;
|
||||||
|
d9 &= 0x1fff;
|
||||||
|
d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0;
|
||||||
|
c += d9 >>> 13;
|
||||||
|
d9 &= 0x1fff;
|
||||||
|
c = ((c << 2) + c) | 0;
|
||||||
|
c = (c + d0) | 0;
|
||||||
|
d0 = c & 0x1fff;
|
||||||
|
c = c >>> 13;
|
||||||
|
d1 += c;
|
||||||
|
h[0] = d0;
|
||||||
|
h[1] = d1;
|
||||||
|
h[2] = d2;
|
||||||
|
h[3] = d3;
|
||||||
|
h[4] = d4;
|
||||||
|
h[5] = d5;
|
||||||
|
h[6] = d6;
|
||||||
|
h[7] = d7;
|
||||||
|
h[8] = d8;
|
||||||
|
h[9] = d9;
|
||||||
|
}
|
||||||
|
finalize() {
|
||||||
|
const { h, pad } = this;
|
||||||
|
const g = new Uint16Array(10);
|
||||||
|
let c = h[1] >>> 13;
|
||||||
|
h[1] &= 0x1fff;
|
||||||
|
for (let i = 2; i < 10; i++) {
|
||||||
|
h[i] += c;
|
||||||
|
c = h[i] >>> 13;
|
||||||
|
h[i] &= 0x1fff;
|
||||||
|
}
|
||||||
|
h[0] += c * 5;
|
||||||
|
c = h[0] >>> 13;
|
||||||
|
h[0] &= 0x1fff;
|
||||||
|
h[1] += c;
|
||||||
|
c = h[1] >>> 13;
|
||||||
|
h[1] &= 0x1fff;
|
||||||
|
h[2] += c;
|
||||||
|
g[0] = h[0] + 5;
|
||||||
|
c = g[0] >>> 13;
|
||||||
|
g[0] &= 0x1fff;
|
||||||
|
for (let i = 1; i < 10; i++) {
|
||||||
|
g[i] = h[i] + c;
|
||||||
|
c = g[i] >>> 13;
|
||||||
|
g[i] &= 0x1fff;
|
||||||
|
}
|
||||||
|
g[9] -= 1 << 13;
|
||||||
|
let mask = (c ^ 1) - 1;
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
g[i] &= mask;
|
||||||
|
mask = ~mask;
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
h[i] = (h[i] & mask) | g[i];
|
||||||
|
h[0] = (h[0] | (h[1] << 13)) & 0xffff;
|
||||||
|
h[1] = ((h[1] >>> 3) | (h[2] << 10)) & 0xffff;
|
||||||
|
h[2] = ((h[2] >>> 6) | (h[3] << 7)) & 0xffff;
|
||||||
|
h[3] = ((h[3] >>> 9) | (h[4] << 4)) & 0xffff;
|
||||||
|
h[4] = ((h[4] >>> 12) | (h[5] << 1) | (h[6] << 14)) & 0xffff;
|
||||||
|
h[5] = ((h[6] >>> 2) | (h[7] << 11)) & 0xffff;
|
||||||
|
h[6] = ((h[7] >>> 5) | (h[8] << 8)) & 0xffff;
|
||||||
|
h[7] = ((h[8] >>> 8) | (h[9] << 5)) & 0xffff;
|
||||||
|
let f = h[0] + pad[0];
|
||||||
|
h[0] = f & 0xffff;
|
||||||
|
for (let i = 1; i < 8; i++) {
|
||||||
|
f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0;
|
||||||
|
h[i] = f & 0xffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
aexists(this);
|
||||||
|
const { buffer, blockLen } = this;
|
||||||
|
data = toBytes(data);
|
||||||
|
const len = data.length;
|
||||||
|
for (let pos = 0; pos < len;) {
|
||||||
|
const take = Math.min(blockLen - this.pos, len - pos);
|
||||||
|
// Fast path: we have at least one block in input
|
||||||
|
if (take === blockLen) {
|
||||||
|
for (; blockLen <= len - pos; pos += blockLen)
|
||||||
|
this.process(data, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer.set(data.subarray(pos, pos + take), this.pos);
|
||||||
|
this.pos += take;
|
||||||
|
pos += take;
|
||||||
|
if (this.pos === blockLen) {
|
||||||
|
this.process(buffer, 0, false);
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this.h.fill(0);
|
||||||
|
this.r.fill(0);
|
||||||
|
this.buffer.fill(0);
|
||||||
|
this.pad.fill(0);
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
aoutput(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
const { buffer, h } = this;
|
||||||
|
let { pos } = this;
|
||||||
|
if (pos) {
|
||||||
|
buffer[pos++] = 1;
|
||||||
|
// buffer.subarray(pos).fill(0);
|
||||||
|
for (; pos < 16; pos++)
|
||||||
|
buffer[pos] = 0;
|
||||||
|
this.process(buffer, 0, true);
|
||||||
|
}
|
||||||
|
this.finalize();
|
||||||
|
let opos = 0;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
out[opos++] = h[i] >>> 0;
|
||||||
|
out[opos++] = h[i] >>> 8;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const { buffer, outputLen } = this;
|
||||||
|
this.digestInto(buffer);
|
||||||
|
const res = buffer.slice(0, outputLen);
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function wrapConstructorWithKey(hashCons) {
|
||||||
|
const hashC = (msg, key) => hashCons(key).update(toBytes(msg)).digest();
|
||||||
|
const tmp = hashCons(new Uint8Array(32));
|
||||||
|
hashC.outputLen = tmp.outputLen;
|
||||||
|
hashC.blockLen = tmp.blockLen;
|
||||||
|
hashC.create = (key) => hashCons(key);
|
||||||
|
return hashC;
|
||||||
|
}
|
||||||
|
export const poly1305 = wrapConstructorWithKey((key) => new Poly1305(key));
|
||||||
|
//# sourceMappingURL=_poly1305.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/_poly1305.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/_poly1305.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
217
node_modules/@noble/ciphers/esm/_polyval.js
generated
vendored
Normal file
217
node_modules/@noble/ciphers/esm/_polyval.js
generated
vendored
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { createView, toBytes, u32 } from './utils.js';
|
||||||
|
import { bytes as abytes, exists as aexists, output as aoutput } from './_assert.js';
|
||||||
|
// GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV.
|
||||||
|
// Implemented in terms of GHash with conversion function for keys
|
||||||
|
// GCM GHASH from NIST SP800-38d, SIV from RFC 8452.
|
||||||
|
// https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
||||||
|
// GHASH modulo: x^128 + x^7 + x^2 + x + 1
|
||||||
|
// POLYVAL modulo: x^128 + x^127 + x^126 + x^121 + 1
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
// TODO: rewrite
|
||||||
|
// temporary padding buffer
|
||||||
|
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
||||||
|
const ZEROS32 = u32(ZEROS16);
|
||||||
|
const POLY = 0xe1; // v = 2*v % POLY
|
||||||
|
// v = 2*v % POLY
|
||||||
|
// NOTE: because x + x = 0 (add/sub is same), mul2(x) != x+x
|
||||||
|
// We can multiply any number using montgomery ladder and this function (works as double, add is simple xor)
|
||||||
|
const mul2 = (s0, s1, s2, s3) => {
|
||||||
|
const hiBit = s3 & 1;
|
||||||
|
return {
|
||||||
|
s3: (s2 << 31) | (s3 >>> 1),
|
||||||
|
s2: (s1 << 31) | (s2 >>> 1),
|
||||||
|
s1: (s0 << 31) | (s1 >>> 1),
|
||||||
|
s0: (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)), // reduce % poly
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const swapLE = (n) => (((n >>> 0) & 0xff) << 24) |
|
||||||
|
(((n >>> 8) & 0xff) << 16) |
|
||||||
|
(((n >>> 16) & 0xff) << 8) |
|
||||||
|
((n >>> 24) & 0xff) |
|
||||||
|
0;
|
||||||
|
/**
|
||||||
|
* `mulX_POLYVAL(ByteReverse(H))` from spec
|
||||||
|
* @param k mutated in place
|
||||||
|
*/
|
||||||
|
export function _toGHASHKey(k) {
|
||||||
|
k.reverse();
|
||||||
|
const hiBit = k[15] & 1;
|
||||||
|
// k >>= 1
|
||||||
|
let carry = 0;
|
||||||
|
for (let i = 0; i < k.length; i++) {
|
||||||
|
const t = k[i];
|
||||||
|
k[i] = (t >>> 1) | carry;
|
||||||
|
carry = (t & 1) << 7;
|
||||||
|
}
|
||||||
|
k[0] ^= -hiBit & 0xe1; // if (hiBit) n ^= 0xe1000000000000000000000000000000;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
const estimateWindow = (bytes) => {
|
||||||
|
if (bytes > 64 * 1024)
|
||||||
|
return 8;
|
||||||
|
if (bytes > 1024)
|
||||||
|
return 4;
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
class GHASH {
|
||||||
|
// We select bits per window adaptively based on expectedLength
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
this.blockLen = BLOCK_SIZE;
|
||||||
|
this.outputLen = BLOCK_SIZE;
|
||||||
|
this.s0 = 0;
|
||||||
|
this.s1 = 0;
|
||||||
|
this.s2 = 0;
|
||||||
|
this.s3 = 0;
|
||||||
|
this.finished = false;
|
||||||
|
key = toBytes(key);
|
||||||
|
abytes(key, 16);
|
||||||
|
const kView = createView(key);
|
||||||
|
let k0 = kView.getUint32(0, false);
|
||||||
|
let k1 = kView.getUint32(4, false);
|
||||||
|
let k2 = kView.getUint32(8, false);
|
||||||
|
let k3 = kView.getUint32(12, false);
|
||||||
|
// generate table of doubled keys (half of montgomery ladder)
|
||||||
|
const doubles = [];
|
||||||
|
for (let i = 0; i < 128; i++) {
|
||||||
|
doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) });
|
||||||
|
({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3));
|
||||||
|
}
|
||||||
|
const W = estimateWindow(expectedLength || 1024);
|
||||||
|
if (![1, 2, 4, 8].includes(W))
|
||||||
|
throw new Error(`ghash: wrong window size=${W}, should be 2, 4 or 8`);
|
||||||
|
this.W = W;
|
||||||
|
const bits = 128; // always 128 bits;
|
||||||
|
const windows = bits / W;
|
||||||
|
const windowSize = (this.windowSize = 2 ** W);
|
||||||
|
const items = [];
|
||||||
|
// Create precompute table for window of W bits
|
||||||
|
for (let w = 0; w < windows; w++) {
|
||||||
|
// truth table: 00, 01, 10, 11
|
||||||
|
for (let byte = 0; byte < windowSize; byte++) {
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
||||||
|
for (let j = 0; j < W; j++) {
|
||||||
|
const bit = (byte >>> (W - j - 1)) & 1;
|
||||||
|
if (!bit)
|
||||||
|
continue;
|
||||||
|
const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j];
|
||||||
|
(s0 ^= d0), (s1 ^= d1), (s2 ^= d2), (s3 ^= d3);
|
||||||
|
}
|
||||||
|
items.push({ s0, s1, s2, s3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.t = items;
|
||||||
|
}
|
||||||
|
_updateBlock(s0, s1, s2, s3) {
|
||||||
|
(s0 ^= this.s0), (s1 ^= this.s1), (s2 ^= this.s2), (s3 ^= this.s3);
|
||||||
|
const { W, t, windowSize } = this;
|
||||||
|
// prettier-ignore
|
||||||
|
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
|
||||||
|
const mask = (1 << W) - 1; // 2**W will kill performance.
|
||||||
|
let w = 0;
|
||||||
|
for (const num of [s0, s1, s2, s3]) {
|
||||||
|
for (let bytePos = 0; bytePos < 4; bytePos++) {
|
||||||
|
const byte = (num >>> (8 * bytePos)) & 0xff;
|
||||||
|
for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) {
|
||||||
|
const bit = (byte >>> (W * bitPos)) & mask;
|
||||||
|
const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit];
|
||||||
|
(o0 ^= e0), (o1 ^= e1), (o2 ^= e2), (o3 ^= e3);
|
||||||
|
w += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.s0 = o0;
|
||||||
|
this.s1 = o1;
|
||||||
|
this.s2 = o2;
|
||||||
|
this.s3 = o3;
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
data = toBytes(data);
|
||||||
|
aexists(this);
|
||||||
|
const b32 = u32(data);
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
this._updateBlock(b32[i * 4 + 0], b32[i * 4 + 1], b32[i * 4 + 2], b32[i * 4 + 3]);
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]);
|
||||||
|
ZEROS32.fill(0); // clean tmp buffer
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
const { t } = this;
|
||||||
|
// clean precompute table
|
||||||
|
for (const elm of t) {
|
||||||
|
(elm.s0 = 0), (elm.s1 = 0), (elm.s2 = 0), (elm.s3 = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
aoutput(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = u32(out);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const res = new Uint8Array(BLOCK_SIZE);
|
||||||
|
this.digestInto(res);
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Polyval extends GHASH {
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
key = toBytes(key);
|
||||||
|
const ghKey = _toGHASHKey(key.slice());
|
||||||
|
super(ghKey, expectedLength);
|
||||||
|
ghKey.fill(0);
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
data = toBytes(data);
|
||||||
|
aexists(this);
|
||||||
|
const b32 = u32(data);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
this._updateBlock(swapLE(b32[i * 4 + 3]), swapLE(b32[i * 4 + 2]), swapLE(b32[i * 4 + 1]), swapLE(b32[i * 4 + 0]));
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
this._updateBlock(swapLE(ZEROS32[3]), swapLE(ZEROS32[2]), swapLE(ZEROS32[1]), swapLE(ZEROS32[0]));
|
||||||
|
ZEROS32.fill(0); // clean tmp buffer
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
aoutput(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
// tmp ugly hack
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = u32(out);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
return out.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function wrapConstructorWithKey(hashCons) {
|
||||||
|
const hashC = (msg, key) => hashCons(key, msg.length).update(toBytes(msg)).digest();
|
||||||
|
const tmp = hashCons(new Uint8Array(16), 0);
|
||||||
|
hashC.outputLen = tmp.outputLen;
|
||||||
|
hashC.blockLen = tmp.blockLen;
|
||||||
|
hashC.create = (key, expectedLength) => hashCons(key, expectedLength);
|
||||||
|
return hashC;
|
||||||
|
}
|
||||||
|
export const ghash = wrapConstructorWithKey((key, expectedLength) => new GHASH(key, expectedLength));
|
||||||
|
export const polyval = wrapConstructorWithKey((key, expectedLength) => new Polyval(key, expectedLength));
|
||||||
|
//# sourceMappingURL=_polyval.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/_polyval.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/_polyval.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
670
node_modules/@noble/ciphers/esm/aes.js
generated
vendored
Normal file
670
node_modules/@noble/ciphers/esm/aes.js
generated
vendored
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
// prettier-ignore
|
||||||
|
import { wrapCipher, createView, setBigUint64, equalBytes, u32, u8, } from './utils.js';
|
||||||
|
import { ghash, polyval } from './_polyval.js';
|
||||||
|
import { bytes as abytes } from './_assert.js';
|
||||||
|
/*
|
||||||
|
AES (Advanced Encryption Standard) aka Rijndael block cipher.
|
||||||
|
|
||||||
|
Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256 bits). In every round:
|
||||||
|
1. **S-box**, table substitution
|
||||||
|
2. **Shift rows**, cyclic shift left of all rows of data array
|
||||||
|
3. **Mix columns**, multiplying every column by fixed polynomial
|
||||||
|
4. **Add round key**, round_key xor i-th column of array
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
- FIPS-197 https://csrc.nist.gov/files/pubs/fips/197/final/docs/fips-197.pdf
|
||||||
|
- Original proposal: https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf
|
||||||
|
*/
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
const BLOCK_SIZE32 = 4;
|
||||||
|
const EMPTY_BLOCK = new Uint8Array(BLOCK_SIZE);
|
||||||
|
const POLY = 0x11b; // 1 + x + x**3 + x**4 + x**8
|
||||||
|
// TODO: remove multiplication, binary ops only
|
||||||
|
function mul2(n) {
|
||||||
|
return (n << 1) ^ (POLY & -(n >> 7));
|
||||||
|
}
|
||||||
|
function mul(a, b) {
|
||||||
|
let res = 0;
|
||||||
|
for (; b > 0; b >>= 1) {
|
||||||
|
// Montgomery ladder
|
||||||
|
res ^= a & -(b & 1); // if (b&1) res ^=a (but const-time).
|
||||||
|
a = mul2(a); // a = 2*a
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// AES S-box is generated using finite field inversion,
|
||||||
|
// an affine transform, and xor of a constant 0x63.
|
||||||
|
const sbox = /* @__PURE__ */ (() => {
|
||||||
|
let t = new Uint8Array(256);
|
||||||
|
for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x))
|
||||||
|
t[i] = x;
|
||||||
|
const box = new Uint8Array(256);
|
||||||
|
box[0] = 0x63; // first elm
|
||||||
|
for (let i = 0; i < 255; i++) {
|
||||||
|
let x = t[255 - i];
|
||||||
|
x |= x << 8;
|
||||||
|
box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff;
|
||||||
|
}
|
||||||
|
return box;
|
||||||
|
})();
|
||||||
|
// Inverted S-box
|
||||||
|
const invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
|
||||||
|
// Rotate u32 by 8
|
||||||
|
const rotr32_8 = (n) => (n << 24) | (n >>> 8);
|
||||||
|
const rotl32_8 = (n) => (n << 8) | (n >>> 24);
|
||||||
|
// T-table is optimization suggested in 5.2 of original proposal (missed from FIPS-197). Changes:
|
||||||
|
// - LE instead of BE
|
||||||
|
// - bigger tables: T0 and T1 are merged into T01 table and T2 & T3 into T23;
|
||||||
|
// so index is u16, instead of u8. This speeds up things, unexpectedly
|
||||||
|
function genTtable(sbox, fn) {
|
||||||
|
if (sbox.length !== 256)
|
||||||
|
throw new Error('Wrong sbox length');
|
||||||
|
const T0 = new Uint32Array(256).map((_, j) => fn(sbox[j]));
|
||||||
|
const T1 = T0.map(rotl32_8);
|
||||||
|
const T2 = T1.map(rotl32_8);
|
||||||
|
const T3 = T2.map(rotl32_8);
|
||||||
|
const T01 = new Uint32Array(256 * 256);
|
||||||
|
const T23 = new Uint32Array(256 * 256);
|
||||||
|
const sbox2 = new Uint16Array(256 * 256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
for (let j = 0; j < 256; j++) {
|
||||||
|
const idx = i * 256 + j;
|
||||||
|
T01[idx] = T0[i] ^ T1[j];
|
||||||
|
T23[idx] = T2[i] ^ T3[j];
|
||||||
|
sbox2[idx] = (sbox[i] << 8) | sbox[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { sbox, sbox2, T0, T1, T2, T3, T01, T23 };
|
||||||
|
}
|
||||||
|
const tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => (mul(s, 3) << 24) | (s << 16) | (s << 8) | mul(s, 2));
|
||||||
|
const tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => (mul(s, 11) << 24) | (mul(s, 13) << 16) | (mul(s, 9) << 8) | mul(s, 14));
|
||||||
|
const xPowers = /* @__PURE__ */ (() => {
|
||||||
|
const p = new Uint8Array(16);
|
||||||
|
for (let i = 0, x = 1; i < 16; i++, x = mul2(x))
|
||||||
|
p[i] = x;
|
||||||
|
return p;
|
||||||
|
})();
|
||||||
|
export function expandKeyLE(key) {
|
||||||
|
abytes(key);
|
||||||
|
const len = key.length;
|
||||||
|
if (![16, 24, 32].includes(len))
|
||||||
|
throw new Error(`aes: wrong key size: should be 16, 24 or 32, got: ${len}`);
|
||||||
|
const { sbox2 } = tableEncoding;
|
||||||
|
const k32 = u32(key);
|
||||||
|
const Nk = k32.length;
|
||||||
|
const subByte = (n) => applySbox(sbox2, n, n, n, n);
|
||||||
|
const xk = new Uint32Array(len + 28); // expanded key
|
||||||
|
xk.set(k32);
|
||||||
|
// 4.3.1 Key expansion
|
||||||
|
for (let i = Nk; i < xk.length; i++) {
|
||||||
|
let t = xk[i - 1];
|
||||||
|
if (i % Nk === 0)
|
||||||
|
t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1];
|
||||||
|
else if (Nk > 6 && i % Nk === 4)
|
||||||
|
t = subByte(t);
|
||||||
|
xk[i] = xk[i - Nk] ^ t;
|
||||||
|
}
|
||||||
|
return xk;
|
||||||
|
}
|
||||||
|
export function expandKeyDecLE(key) {
|
||||||
|
const encKey = expandKeyLE(key);
|
||||||
|
const xk = encKey.slice();
|
||||||
|
const Nk = encKey.length;
|
||||||
|
const { sbox2 } = tableEncoding;
|
||||||
|
const { T0, T1, T2, T3 } = tableDecoding;
|
||||||
|
// Inverse key by chunks of 4 (rounds)
|
||||||
|
for (let i = 0; i < Nk; i += 4) {
|
||||||
|
for (let j = 0; j < 4; j++)
|
||||||
|
xk[i + j] = encKey[Nk - i - 4 + j];
|
||||||
|
}
|
||||||
|
encKey.fill(0);
|
||||||
|
// apply InvMixColumn except first & last round
|
||||||
|
for (let i = 4; i < Nk - 4; i++) {
|
||||||
|
const x = xk[i];
|
||||||
|
const w = applySbox(sbox2, x, x, x, x);
|
||||||
|
xk[i] = T0[w & 0xff] ^ T1[(w >>> 8) & 0xff] ^ T2[(w >>> 16) & 0xff] ^ T3[w >>> 24];
|
||||||
|
}
|
||||||
|
return xk;
|
||||||
|
}
|
||||||
|
// Apply tables
|
||||||
|
function apply0123(T01, T23, s0, s1, s2, s3) {
|
||||||
|
return (T01[((s0 << 8) & 0xff00) | ((s1 >>> 8) & 0xff)] ^
|
||||||
|
T23[((s2 >>> 8) & 0xff00) | ((s3 >>> 24) & 0xff)]);
|
||||||
|
}
|
||||||
|
function applySbox(sbox2, s0, s1, s2, s3) {
|
||||||
|
return (sbox2[(s0 & 0xff) | (s1 & 0xff00)] |
|
||||||
|
(sbox2[((s2 >>> 16) & 0xff) | ((s3 >>> 16) & 0xff00)] << 16));
|
||||||
|
}
|
||||||
|
function encrypt(xk, s0, s1, s2, s3) {
|
||||||
|
const { sbox2, T01, T23 } = tableEncoding;
|
||||||
|
let k = 0;
|
||||||
|
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||||||
|
const rounds = xk.length / 4 - 2;
|
||||||
|
for (let i = 0; i < rounds; i++) {
|
||||||
|
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3);
|
||||||
|
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0);
|
||||||
|
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1);
|
||||||
|
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2);
|
||||||
|
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||||||
|
}
|
||||||
|
// last round (without mixcolumns, so using SBOX2 table)
|
||||||
|
const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3);
|
||||||
|
const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0);
|
||||||
|
const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1);
|
||||||
|
const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2);
|
||||||
|
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||||||
|
}
|
||||||
|
function decrypt(xk, s0, s1, s2, s3) {
|
||||||
|
const { sbox2, T01, T23 } = tableDecoding;
|
||||||
|
let k = 0;
|
||||||
|
(s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]);
|
||||||
|
const rounds = xk.length / 4 - 2;
|
||||||
|
for (let i = 0; i < rounds; i++) {
|
||||||
|
const t0 = xk[k++] ^ apply0123(T01, T23, s0, s3, s2, s1);
|
||||||
|
const t1 = xk[k++] ^ apply0123(T01, T23, s1, s0, s3, s2);
|
||||||
|
const t2 = xk[k++] ^ apply0123(T01, T23, s2, s1, s0, s3);
|
||||||
|
const t3 = xk[k++] ^ apply0123(T01, T23, s3, s2, s1, s0);
|
||||||
|
(s0 = t0), (s1 = t1), (s2 = t2), (s3 = t3);
|
||||||
|
}
|
||||||
|
// Last round
|
||||||
|
const t0 = xk[k++] ^ applySbox(sbox2, s0, s3, s2, s1);
|
||||||
|
const t1 = xk[k++] ^ applySbox(sbox2, s1, s0, s3, s2);
|
||||||
|
const t2 = xk[k++] ^ applySbox(sbox2, s2, s1, s0, s3);
|
||||||
|
const t3 = xk[k++] ^ applySbox(sbox2, s3, s2, s1, s0);
|
||||||
|
return { s0: t0, s1: t1, s2: t2, s3: t3 };
|
||||||
|
}
|
||||||
|
function getDst(len, dst) {
|
||||||
|
if (!dst)
|
||||||
|
return new Uint8Array(len);
|
||||||
|
abytes(dst);
|
||||||
|
if (dst.length < len)
|
||||||
|
throw new Error(`aes: wrong destination length, expected at least ${len}, got: ${dst.length}`);
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
// TODO: investigate merging with ctr32
|
||||||
|
function ctrCounter(xk, nonce, src, dst) {
|
||||||
|
abytes(nonce, BLOCK_SIZE);
|
||||||
|
abytes(src);
|
||||||
|
const srcLen = src.length;
|
||||||
|
dst = getDst(srcLen, dst);
|
||||||
|
const ctr = nonce;
|
||||||
|
const c32 = u32(ctr);
|
||||||
|
// Fill block (empty, ctr=0)
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||||||
|
const src32 = u32(src);
|
||||||
|
const dst32 = u32(dst);
|
||||||
|
// process blocks
|
||||||
|
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ s0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ s1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ s2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ s3;
|
||||||
|
// Full 128 bit counter with wrap around
|
||||||
|
let carry = 1;
|
||||||
|
for (let i = ctr.length - 1; i >= 0; i--) {
|
||||||
|
carry = (carry + (ctr[i] & 0xff)) | 0;
|
||||||
|
ctr[i] = carry & 0xff;
|
||||||
|
carry >>>= 8;
|
||||||
|
}
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||||||
|
}
|
||||||
|
// leftovers (less than block)
|
||||||
|
// It's possible to handle > u32 fast, but is it worth it?
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||||||
|
const buf = u8(b32);
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
// AES CTR with overflowing 32 bit counter
|
||||||
|
// It's possible to do 32le significantly simpler (and probably faster) by using u32.
|
||||||
|
// But, we need both, and perf bottleneck is in ghash anyway.
|
||||||
|
function ctr32(xk, isLE, nonce, src, dst) {
|
||||||
|
abytes(nonce, BLOCK_SIZE);
|
||||||
|
abytes(src);
|
||||||
|
dst = getDst(src.length, dst);
|
||||||
|
const ctr = nonce; // write new value to nonce, so it can be re-used
|
||||||
|
const c32 = u32(ctr);
|
||||||
|
const view = createView(ctr);
|
||||||
|
const src32 = u32(src);
|
||||||
|
const dst32 = u32(dst);
|
||||||
|
const ctrPos = isLE ? 0 : 12;
|
||||||
|
const srcLen = src.length;
|
||||||
|
// Fill block (empty, ctr=0)
|
||||||
|
let ctrNum = view.getUint32(ctrPos, isLE); // read current counter value
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]);
|
||||||
|
// process blocks
|
||||||
|
for (let i = 0; i + 4 <= src32.length; i += 4) {
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ s0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ s1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ s2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ s3;
|
||||||
|
ctrNum = (ctrNum + 1) >>> 0; // u32 wrap
|
||||||
|
view.setUint32(ctrPos, ctrNum, isLE);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, c32[0], c32[1], c32[2], c32[3]));
|
||||||
|
}
|
||||||
|
// leftovers (less than a block)
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
const b32 = new Uint32Array([s0, s1, s2, s3]);
|
||||||
|
const buf = u8(b32);
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* CTR: counter mode. Creates stream cipher.
|
||||||
|
* Requires good IV. Parallelizable. OK, but no MAC.
|
||||||
|
*/
|
||||||
|
export const ctr = wrapCipher({ blockSize: 16, nonceLength: 16 }, function ctr(key, nonce) {
|
||||||
|
abytes(key);
|
||||||
|
abytes(nonce, BLOCK_SIZE);
|
||||||
|
function processCtr(buf, dst) {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const n = nonce.slice();
|
||||||
|
const out = ctrCounter(xk, n, buf, dst);
|
||||||
|
xk.fill(0);
|
||||||
|
n.fill(0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => processCtr(plaintext, dst),
|
||||||
|
decrypt: (ciphertext, dst) => processCtr(ciphertext, dst),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
function validateBlockDecrypt(data) {
|
||||||
|
abytes(data);
|
||||||
|
if (data.length % BLOCK_SIZE !== 0) {
|
||||||
|
throw new Error(`aes/(cbc-ecb).decrypt ciphertext should consist of blocks with size ${BLOCK_SIZE}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateBlockEncrypt(plaintext, pcks5, dst) {
|
||||||
|
let outLen = plaintext.length;
|
||||||
|
const remaining = outLen % BLOCK_SIZE;
|
||||||
|
if (!pcks5 && remaining !== 0)
|
||||||
|
throw new Error('aec/(cbc-ecb): unpadded plaintext with disabled padding');
|
||||||
|
const b = u32(plaintext);
|
||||||
|
if (pcks5) {
|
||||||
|
let left = BLOCK_SIZE - remaining;
|
||||||
|
if (!left)
|
||||||
|
left = BLOCK_SIZE; // if no bytes left, create empty padding block
|
||||||
|
outLen = outLen + left;
|
||||||
|
}
|
||||||
|
const out = getDst(outLen, dst);
|
||||||
|
const o = u32(out);
|
||||||
|
return { b, o, out };
|
||||||
|
}
|
||||||
|
function validatePCKS(data, pcks5) {
|
||||||
|
if (!pcks5)
|
||||||
|
return data;
|
||||||
|
const len = data.length;
|
||||||
|
if (!len)
|
||||||
|
throw new Error(`aes/pcks5: empty ciphertext not allowed`);
|
||||||
|
const lastByte = data[len - 1];
|
||||||
|
if (lastByte <= 0 || lastByte > 16)
|
||||||
|
throw new Error(`aes/pcks5: wrong padding byte: ${lastByte}`);
|
||||||
|
const out = data.subarray(0, -lastByte);
|
||||||
|
for (let i = 0; i < lastByte; i++)
|
||||||
|
if (data[len - i - 1] !== lastByte)
|
||||||
|
throw new Error(`aes/pcks5: wrong padding`);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function padPCKS(left) {
|
||||||
|
const tmp = new Uint8Array(16);
|
||||||
|
const tmp32 = u32(tmp);
|
||||||
|
tmp.set(left);
|
||||||
|
const paddingByte = BLOCK_SIZE - left.length;
|
||||||
|
for (let i = BLOCK_SIZE - paddingByte; i < BLOCK_SIZE; i++)
|
||||||
|
tmp[i] = paddingByte;
|
||||||
|
return tmp32;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* ECB: Electronic CodeBook. Simple deterministic replacement.
|
||||||
|
* Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/).
|
||||||
|
*/
|
||||||
|
export const ecb = wrapCipher({ blockSize: 16 }, function ecb(key, opts = {}) {
|
||||||
|
abytes(key);
|
||||||
|
const pcks5 = !opts.disablePadding;
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => {
|
||||||
|
abytes(plaintext);
|
||||||
|
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
let i = 0;
|
||||||
|
for (; i + 4 <= b.length;) {
|
||||||
|
const { s0, s1, s2, s3 } = encrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
if (pcks5) {
|
||||||
|
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||||||
|
const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return _out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, dst) => {
|
||||||
|
validateBlockDecrypt(ciphertext);
|
||||||
|
const xk = expandKeyDecLE(key);
|
||||||
|
const out = getDst(ciphertext.length, dst);
|
||||||
|
const b = u32(ciphertext);
|
||||||
|
const o = u32(out);
|
||||||
|
for (let i = 0; i + 4 <= b.length;) {
|
||||||
|
const { s0, s1, s2, s3 } = decrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]);
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return validatePCKS(out, pcks5);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* CBC: Cipher-Block-Chaining. Key is previous round’s block.
|
||||||
|
* Fragile: needs proper padding. Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
export const cbc = wrapCipher({ blockSize: 16, nonceLength: 16 }, function cbc(key, iv, opts = {}) {
|
||||||
|
abytes(key);
|
||||||
|
abytes(iv, 16);
|
||||||
|
const pcks5 = !opts.disablePadding;
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst);
|
||||||
|
const n32 = u32(iv);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
let i = 0;
|
||||||
|
for (; i + 4 <= b.length;) {
|
||||||
|
(s0 ^= b[i + 0]), (s1 ^= b[i + 1]), (s2 ^= b[i + 2]), (s3 ^= b[i + 3]);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
if (pcks5) {
|
||||||
|
const tmp32 = padPCKS(plaintext.subarray(i * 4));
|
||||||
|
(s0 ^= tmp32[0]), (s1 ^= tmp32[1]), (s2 ^= tmp32[2]), (s3 ^= tmp32[3]);
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
(o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return _out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, dst) => {
|
||||||
|
validateBlockDecrypt(ciphertext);
|
||||||
|
const xk = expandKeyDecLE(key);
|
||||||
|
const n32 = u32(iv);
|
||||||
|
const out = getDst(ciphertext.length, dst);
|
||||||
|
const b = u32(ciphertext);
|
||||||
|
const o = u32(out);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
for (let i = 0; i + 4 <= b.length;) {
|
||||||
|
// prettier-ignore
|
||||||
|
const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3;
|
||||||
|
(s0 = b[i + 0]), (s1 = b[i + 1]), (s2 = b[i + 2]), (s3 = b[i + 3]);
|
||||||
|
const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3);
|
||||||
|
(o[i++] = o0 ^ ps0), (o[i++] = o1 ^ ps1), (o[i++] = o2 ^ ps2), (o[i++] = o3 ^ ps3);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return validatePCKS(out, pcks5);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* CFB: Cipher Feedback Mode. The input for the block cipher is the previous cipher output.
|
||||||
|
* Unauthenticated: needs MAC.
|
||||||
|
*/
|
||||||
|
export const cfb = wrapCipher({ blockSize: 16, nonceLength: 16 }, function cfb(key, iv) {
|
||||||
|
abytes(key);
|
||||||
|
abytes(iv, 16);
|
||||||
|
function processCfb(src, isEncrypt, dst) {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const srcLen = src.length;
|
||||||
|
dst = getDst(srcLen, dst);
|
||||||
|
const src32 = u32(src);
|
||||||
|
const dst32 = u32(dst);
|
||||||
|
const next32 = isEncrypt ? dst32 : src32;
|
||||||
|
const n32 = u32(iv);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3];
|
||||||
|
for (let i = 0; i + 4 <= src32.length;) {
|
||||||
|
const { s0: e0, s1: e1, s2: e2, s3: e3 } = encrypt(xk, s0, s1, s2, s3);
|
||||||
|
dst32[i + 0] = src32[i + 0] ^ e0;
|
||||||
|
dst32[i + 1] = src32[i + 1] ^ e1;
|
||||||
|
dst32[i + 2] = src32[i + 2] ^ e2;
|
||||||
|
dst32[i + 3] = src32[i + 3] ^ e3;
|
||||||
|
(s0 = next32[i++]), (s1 = next32[i++]), (s2 = next32[i++]), (s3 = next32[i++]);
|
||||||
|
}
|
||||||
|
// leftovers (less than block)
|
||||||
|
const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32);
|
||||||
|
if (start < srcLen) {
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3));
|
||||||
|
const buf = u8(new Uint32Array([s0, s1, s2, s3]));
|
||||||
|
for (let i = start, pos = 0; i < srcLen; i++, pos++)
|
||||||
|
dst[i] = src[i] ^ buf[pos];
|
||||||
|
buf.fill(0);
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, dst) => processCfb(plaintext, true, dst),
|
||||||
|
decrypt: (ciphertext, dst) => processCfb(ciphertext, false, dst),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// TODO: merge with chacha, however gcm has bitLen while chacha has byteLen
|
||||||
|
function computeTag(fn, isLE, key, data, AAD) {
|
||||||
|
const h = fn.create(key, data.length + (AAD?.length || 0));
|
||||||
|
if (AAD)
|
||||||
|
h.update(AAD);
|
||||||
|
h.update(data);
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = createView(num);
|
||||||
|
if (AAD)
|
||||||
|
setBigUint64(view, 0, BigInt(AAD.length * 8), isLE);
|
||||||
|
setBigUint64(view, 8, BigInt(data.length * 8), isLE);
|
||||||
|
h.update(num);
|
||||||
|
return h.digest();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* GCM: Galois/Counter Mode.
|
||||||
|
* Good, modern version of CTR, parallel, with MAC.
|
||||||
|
* Be careful: MACs can be forged.
|
||||||
|
*/
|
||||||
|
export const gcm = wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function gcm(key, nonce, AAD) {
|
||||||
|
abytes(nonce);
|
||||||
|
// Nonce can be pretty much anything (even 1 byte). But smaller nonces less secure.
|
||||||
|
if (nonce.length === 0)
|
||||||
|
throw new Error('aes/gcm: empty nonce');
|
||||||
|
const tagLength = 16;
|
||||||
|
function _computeTag(authKey, tagMask, data) {
|
||||||
|
const tag = computeTag(ghash, false, authKey, data, AAD);
|
||||||
|
for (let i = 0; i < tagMask.length; i++)
|
||||||
|
tag[i] ^= tagMask[i];
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
function deriveKeys() {
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const authKey = EMPTY_BLOCK.slice();
|
||||||
|
const counter = EMPTY_BLOCK.slice();
|
||||||
|
ctr32(xk, false, counter, counter, authKey);
|
||||||
|
if (nonce.length === 12) {
|
||||||
|
counter.set(nonce);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Spec (NIST 800-38d) supports variable size nonce.
|
||||||
|
// Not supported for now, but can be useful.
|
||||||
|
const nonceLen = EMPTY_BLOCK.slice();
|
||||||
|
const view = createView(nonceLen);
|
||||||
|
setBigUint64(view, 8, BigInt(nonce.length * 8), false);
|
||||||
|
// ghash(nonce || u64be(0) || u64be(nonceLen*8))
|
||||||
|
ghash.create(authKey).update(nonce).update(nonceLen).digestInto(counter);
|
||||||
|
}
|
||||||
|
const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK);
|
||||||
|
return { xk, authKey, counter, tagMask };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
abytes(plaintext);
|
||||||
|
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||||||
|
const out = new Uint8Array(plaintext.length + tagLength);
|
||||||
|
ctr32(xk, false, counter, plaintext, out);
|
||||||
|
const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength));
|
||||||
|
out.set(tag, plaintext.length);
|
||||||
|
xk.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
abytes(ciphertext);
|
||||||
|
if (ciphertext.length < tagLength)
|
||||||
|
throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`);
|
||||||
|
const { xk, authKey, counter, tagMask } = deriveKeys();
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const tag = _computeTag(authKey, tagMask, data);
|
||||||
|
if (!equalBytes(tag, passedTag))
|
||||||
|
throw new Error('aes/gcm: invalid ghash tag');
|
||||||
|
const out = ctr32(xk, false, counter, data);
|
||||||
|
authKey.fill(0);
|
||||||
|
tagMask.fill(0);
|
||||||
|
xk.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const limit = (name, min, max) => (value) => {
|
||||||
|
if (!Number.isSafeInteger(value) || min > value || value > max)
|
||||||
|
throw new Error(`${name}: invalid value=${value}, must be [${min}..${max}]`);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
|
||||||
|
* Guarantees that, when a nonce is repeated, the only security loss is that identical
|
||||||
|
* plaintexts will produce identical ciphertexts.
|
||||||
|
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
|
||||||
|
*/
|
||||||
|
export const siv = wrapCipher({ blockSize: 16, nonceLength: 12, tagLength: 16 }, function siv(key, nonce, AAD) {
|
||||||
|
const tagLength = 16;
|
||||||
|
// From RFC 8452: Section 6
|
||||||
|
const AAD_LIMIT = limit('AAD', 0, 2 ** 36);
|
||||||
|
const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36);
|
||||||
|
const NONCE_LIMIT = limit('nonce', 12, 12);
|
||||||
|
const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16);
|
||||||
|
abytes(nonce);
|
||||||
|
NONCE_LIMIT(nonce.length);
|
||||||
|
if (AAD) {
|
||||||
|
abytes(AAD);
|
||||||
|
AAD_LIMIT(AAD.length);
|
||||||
|
}
|
||||||
|
function deriveKeys() {
|
||||||
|
const len = key.length;
|
||||||
|
if (len !== 16 && len !== 24 && len !== 32)
|
||||||
|
throw new Error(`key length must be 16, 24 or 32 bytes, got: ${len} bytes`);
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const encKey = new Uint8Array(len);
|
||||||
|
const authKey = new Uint8Array(16);
|
||||||
|
const n32 = u32(nonce);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2];
|
||||||
|
let counter = 0;
|
||||||
|
for (const derivedKey of [authKey, encKey].map(u32)) {
|
||||||
|
const d32 = u32(derivedKey);
|
||||||
|
for (let i = 0; i < d32.length; i += 2) {
|
||||||
|
// aes(u32le(0) || nonce)[:8] || aes(u32le(1) || nonce)[:8] ...
|
||||||
|
const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3);
|
||||||
|
d32[i + 0] = o0;
|
||||||
|
d32[i + 1] = o1;
|
||||||
|
s0 = ++counter; // increment counter inside state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xk.fill(0);
|
||||||
|
return { authKey, encKey: expandKeyLE(encKey) };
|
||||||
|
}
|
||||||
|
function _computeTag(encKey, authKey, data) {
|
||||||
|
const tag = computeTag(polyval, true, authKey, data, AAD);
|
||||||
|
// Compute the expected tag by XORing S_s and the nonce, clearing the
|
||||||
|
// most significant bit of the last byte and encrypting with the
|
||||||
|
// message-encryption key.
|
||||||
|
for (let i = 0; i < 12; i++)
|
||||||
|
tag[i] ^= nonce[i];
|
||||||
|
tag[15] &= 0x7f; // Clear the highest bit
|
||||||
|
// encrypt tag as block
|
||||||
|
const t32 = u32(tag);
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3];
|
||||||
|
({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3));
|
||||||
|
(t32[0] = s0), (t32[1] = s1), (t32[2] = s2), (t32[3] = s3);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
// actual decrypt/encrypt of message.
|
||||||
|
function processSiv(encKey, tag, input) {
|
||||||
|
let block = tag.slice();
|
||||||
|
block[15] |= 0x80; // Force highest bit
|
||||||
|
return ctr32(encKey, true, block, input);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext) => {
|
||||||
|
abytes(plaintext);
|
||||||
|
PLAIN_LIMIT(plaintext.length);
|
||||||
|
const { encKey, authKey } = deriveKeys();
|
||||||
|
const tag = _computeTag(encKey, authKey, plaintext);
|
||||||
|
const out = new Uint8Array(plaintext.length + tagLength);
|
||||||
|
out.set(tag, plaintext.length);
|
||||||
|
out.set(processSiv(encKey, tag, plaintext));
|
||||||
|
encKey.fill(0);
|
||||||
|
authKey.fill(0);
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext) => {
|
||||||
|
abytes(ciphertext);
|
||||||
|
CIPHER_LIMIT(ciphertext.length);
|
||||||
|
const tag = ciphertext.subarray(-tagLength);
|
||||||
|
const { encKey, authKey } = deriveKeys();
|
||||||
|
const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength));
|
||||||
|
const expectedTag = _computeTag(encKey, authKey, plaintext);
|
||||||
|
encKey.fill(0);
|
||||||
|
authKey.fill(0);
|
||||||
|
if (!equalBytes(tag, expectedTag))
|
||||||
|
throw new Error('invalid polyval tag');
|
||||||
|
return plaintext;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
function isBytes32(a) {
|
||||||
|
return (a != null &&
|
||||||
|
typeof a === 'object' &&
|
||||||
|
(a instanceof Uint32Array || a.constructor.name === 'Uint32Array'));
|
||||||
|
}
|
||||||
|
function encryptBlock(xk, block) {
|
||||||
|
abytes(block, 16);
|
||||||
|
if (!isBytes32(xk))
|
||||||
|
throw new Error('_encryptBlock accepts result of expandKeyLE');
|
||||||
|
const b32 = u32(block);
|
||||||
|
let { s0, s1, s2, s3 } = encrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||||||
|
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
function decryptBlock(xk, block) {
|
||||||
|
abytes(block, 16);
|
||||||
|
if (!isBytes32(xk))
|
||||||
|
throw new Error('_decryptBlock accepts result of expandKeyLE');
|
||||||
|
const b32 = u32(block);
|
||||||
|
let { s0, s1, s2, s3 } = decrypt(xk, b32[0], b32[1], b32[2], b32[3]);
|
||||||
|
(b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
// Highly unsafe private functions for implementing new modes or ciphers based on AES
|
||||||
|
// Can change at any time, no API guarantees
|
||||||
|
export const unsafe = {
|
||||||
|
expandKeyLE,
|
||||||
|
expandKeyDecLE,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
encryptBlock,
|
||||||
|
decryptBlock,
|
||||||
|
ctrCounter,
|
||||||
|
ctr32,
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=aes.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/aes.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/aes.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
318
node_modules/@noble/ciphers/esm/chacha.js
generated
vendored
Normal file
318
node_modules/@noble/ciphers/esm/chacha.js
generated
vendored
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
// prettier-ignore
|
||||||
|
import { wrapCipher, createView, equalBytes, setBigUint64, } from './utils.js';
|
||||||
|
import { poly1305 } from './_poly1305.js';
|
||||||
|
import { createCipher, rotl } from './_arx.js';
|
||||||
|
import { bytes as abytes } from './_assert.js';
|
||||||
|
// ChaCha20 stream cipher was released in 2008. ChaCha aims to increase
|
||||||
|
// the diffusion per round, but had slightly less cryptanalysis.
|
||||||
|
// https://cr.yp.to/chacha.html, http://cr.yp.to/chacha/chacha-20080128.pdf
|
||||||
|
/**
|
||||||
|
* ChaCha core function.
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
||||||
|
let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], // "expa" "nd 3" "2-by" "te k"
|
||||||
|
y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key
|
||||||
|
y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key
|
||||||
|
y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce
|
||||||
|
// Save state to temporary variables
|
||||||
|
let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
||||||
|
for (let r = 0; r < rounds; r += 2) {
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = rotl(x12 ^ x00, 16);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = rotl(x04 ^ x08, 12);
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = rotl(x12 ^ x00, 8);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = rotl(x04 ^ x08, 7);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = rotl(x13 ^ x01, 16);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = rotl(x05 ^ x09, 12);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = rotl(x13 ^ x01, 8);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = rotl(x05 ^ x09, 7);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = rotl(x14 ^ x02, 16);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = rotl(x06 ^ x10, 12);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = rotl(x14 ^ x02, 8);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = rotl(x06 ^ x10, 7);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = rotl(x15 ^ x03, 16);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = rotl(x07 ^ x11, 12);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = rotl(x15 ^ x03, 8);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = rotl(x07 ^ x11, 7);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = rotl(x15 ^ x00, 16);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = rotl(x05 ^ x10, 12);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = rotl(x15 ^ x00, 8);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = rotl(x05 ^ x10, 7);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = rotl(x12 ^ x01, 16);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = rotl(x06 ^ x11, 12);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = rotl(x12 ^ x01, 8);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = rotl(x06 ^ x11, 7);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = rotl(x13 ^ x02, 16);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = rotl(x07 ^ x08, 12);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = rotl(x13 ^ x02, 8);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = rotl(x07 ^ x08, 7);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = rotl(x14 ^ x03, 16);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = rotl(x04 ^ x09, 12);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = rotl(x14 ^ x03, 8);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = rotl(x04 ^ x09, 7);
|
||||||
|
}
|
||||||
|
// Write output
|
||||||
|
let oi = 0;
|
||||||
|
out[oi++] = (y00 + x00) | 0;
|
||||||
|
out[oi++] = (y01 + x01) | 0;
|
||||||
|
out[oi++] = (y02 + x02) | 0;
|
||||||
|
out[oi++] = (y03 + x03) | 0;
|
||||||
|
out[oi++] = (y04 + x04) | 0;
|
||||||
|
out[oi++] = (y05 + x05) | 0;
|
||||||
|
out[oi++] = (y06 + x06) | 0;
|
||||||
|
out[oi++] = (y07 + x07) | 0;
|
||||||
|
out[oi++] = (y08 + x08) | 0;
|
||||||
|
out[oi++] = (y09 + x09) | 0;
|
||||||
|
out[oi++] = (y10 + x10) | 0;
|
||||||
|
out[oi++] = (y11 + x11) | 0;
|
||||||
|
out[oi++] = (y12 + x12) | 0;
|
||||||
|
out[oi++] = (y13 + x13) | 0;
|
||||||
|
out[oi++] = (y14 + x14) | 0;
|
||||||
|
out[oi++] = (y15 + x15) | 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* hchacha helper method, used primarily in xchacha, to hash
|
||||||
|
* key and nonce into key' and nonce'.
|
||||||
|
* Same as chachaCore, but there doesn't seem to be a way to move the block
|
||||||
|
* out without 25% performance hit.
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
export function hchacha(s, k, i, o32) {
|
||||||
|
let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3];
|
||||||
|
for (let r = 0; r < 20; r += 2) {
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = rotl(x12 ^ x00, 16);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = rotl(x04 ^ x08, 12);
|
||||||
|
x00 = (x00 + x04) | 0;
|
||||||
|
x12 = rotl(x12 ^ x00, 8);
|
||||||
|
x08 = (x08 + x12) | 0;
|
||||||
|
x04 = rotl(x04 ^ x08, 7);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = rotl(x13 ^ x01, 16);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = rotl(x05 ^ x09, 12);
|
||||||
|
x01 = (x01 + x05) | 0;
|
||||||
|
x13 = rotl(x13 ^ x01, 8);
|
||||||
|
x09 = (x09 + x13) | 0;
|
||||||
|
x05 = rotl(x05 ^ x09, 7);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = rotl(x14 ^ x02, 16);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = rotl(x06 ^ x10, 12);
|
||||||
|
x02 = (x02 + x06) | 0;
|
||||||
|
x14 = rotl(x14 ^ x02, 8);
|
||||||
|
x10 = (x10 + x14) | 0;
|
||||||
|
x06 = rotl(x06 ^ x10, 7);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = rotl(x15 ^ x03, 16);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = rotl(x07 ^ x11, 12);
|
||||||
|
x03 = (x03 + x07) | 0;
|
||||||
|
x15 = rotl(x15 ^ x03, 8);
|
||||||
|
x11 = (x11 + x15) | 0;
|
||||||
|
x07 = rotl(x07 ^ x11, 7);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = rotl(x15 ^ x00, 16);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = rotl(x05 ^ x10, 12);
|
||||||
|
x00 = (x00 + x05) | 0;
|
||||||
|
x15 = rotl(x15 ^ x00, 8);
|
||||||
|
x10 = (x10 + x15) | 0;
|
||||||
|
x05 = rotl(x05 ^ x10, 7);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = rotl(x12 ^ x01, 16);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = rotl(x06 ^ x11, 12);
|
||||||
|
x01 = (x01 + x06) | 0;
|
||||||
|
x12 = rotl(x12 ^ x01, 8);
|
||||||
|
x11 = (x11 + x12) | 0;
|
||||||
|
x06 = rotl(x06 ^ x11, 7);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = rotl(x13 ^ x02, 16);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = rotl(x07 ^ x08, 12);
|
||||||
|
x02 = (x02 + x07) | 0;
|
||||||
|
x13 = rotl(x13 ^ x02, 8);
|
||||||
|
x08 = (x08 + x13) | 0;
|
||||||
|
x07 = rotl(x07 ^ x08, 7);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = rotl(x14 ^ x03, 16);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = rotl(x04 ^ x09, 12);
|
||||||
|
x03 = (x03 + x04) | 0;
|
||||||
|
x14 = rotl(x14 ^ x03, 8);
|
||||||
|
x09 = (x09 + x14) | 0;
|
||||||
|
x04 = rotl(x04 ^ x09, 7);
|
||||||
|
}
|
||||||
|
let oi = 0;
|
||||||
|
o32[oi++] = x00;
|
||||||
|
o32[oi++] = x01;
|
||||||
|
o32[oi++] = x02;
|
||||||
|
o32[oi++] = x03;
|
||||||
|
o32[oi++] = x12;
|
||||||
|
o32[oi++] = x13;
|
||||||
|
o32[oi++] = x14;
|
||||||
|
o32[oi++] = x15;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Original, non-RFC chacha20 from DJB. 8-byte nonce, 8-byte counter.
|
||||||
|
*/
|
||||||
|
export const chacha20orig = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
allowShortKeys: true,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* ChaCha stream cipher. Conforms to RFC 8439 (IETF, TLS). 12-byte nonce, 4-byte counter.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
export const chacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
allowShortKeys: false,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* XChaCha eXtended-nonce ChaCha. 24-byte nonce.
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
*/
|
||||||
|
export const xchacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 8,
|
||||||
|
extendNonceFn: hchacha,
|
||||||
|
allowShortKeys: false,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Reduced 8-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
export const chacha8 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 8,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Reduced 12-round chacha, described in original paper.
|
||||||
|
*/
|
||||||
|
export const chacha12 = /* @__PURE__ */ createCipher(chachaCore, {
|
||||||
|
counterRight: false,
|
||||||
|
counterLength: 4,
|
||||||
|
rounds: 12,
|
||||||
|
});
|
||||||
|
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
||||||
|
// Pad to digest size with zeros
|
||||||
|
const updatePadded = (h, msg) => {
|
||||||
|
h.update(msg);
|
||||||
|
const left = msg.length % 16;
|
||||||
|
if (left)
|
||||||
|
h.update(ZEROS16.subarray(left));
|
||||||
|
};
|
||||||
|
const ZEROS32 = /* @__PURE__ */ new Uint8Array(32);
|
||||||
|
function computeTag(fn, key, nonce, data, AAD) {
|
||||||
|
const authKey = fn(key, nonce, ZEROS32);
|
||||||
|
const h = poly1305.create(authKey);
|
||||||
|
if (AAD)
|
||||||
|
updatePadded(h, AAD);
|
||||||
|
updatePadded(h, data);
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = createView(num);
|
||||||
|
setBigUint64(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||||
|
setBigUint64(view, 8, BigInt(data.length), true);
|
||||||
|
h.update(num);
|
||||||
|
const res = h.digest();
|
||||||
|
authKey.fill(0);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* AEAD algorithm from RFC 8439.
|
||||||
|
* Salsa20 and chacha (RFC 8439) use poly1305 differently.
|
||||||
|
* We could have composed them similar to:
|
||||||
|
* https://github.com/paulmillr/scure-base/blob/b266c73dde977b1dd7ef40ef7a23cc15aab526b3/index.ts#L250
|
||||||
|
* But it's hard because of authKey:
|
||||||
|
* In salsa20, authKey changes position in salsa stream.
|
||||||
|
* In chacha, authKey can't be computed inside computeTag, it modifies the counter.
|
||||||
|
*/
|
||||||
|
export const _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
|
||||||
|
const tagLength = 16;
|
||||||
|
abytes(key, 32);
|
||||||
|
abytes(nonce);
|
||||||
|
return {
|
||||||
|
encrypt: (plaintext, output) => {
|
||||||
|
const plength = plaintext.length;
|
||||||
|
const clength = plength + tagLength;
|
||||||
|
if (output) {
|
||||||
|
abytes(output, clength);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
output = new Uint8Array(clength);
|
||||||
|
}
|
||||||
|
xorStream(key, nonce, plaintext, output, 1);
|
||||||
|
const tag = computeTag(xorStream, key, nonce, output.subarray(0, -tagLength), AAD);
|
||||||
|
output.set(tag, plength); // append tag
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
decrypt: (ciphertext, output) => {
|
||||||
|
const clength = ciphertext.length;
|
||||||
|
const plength = clength - tagLength;
|
||||||
|
if (clength < tagLength)
|
||||||
|
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||||
|
if (output) {
|
||||||
|
abytes(output, plength);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
output = new Uint8Array(plength);
|
||||||
|
}
|
||||||
|
const data = ciphertext.subarray(0, -tagLength);
|
||||||
|
const passedTag = ciphertext.subarray(-tagLength);
|
||||||
|
const tag = computeTag(xorStream, key, nonce, data, AAD);
|
||||||
|
if (!equalBytes(passedTag, tag))
|
||||||
|
throw new Error('invalid tag');
|
||||||
|
xorStream(key, nonce, data, output, 1);
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* ChaCha20-Poly1305 from RFC 8439.
|
||||||
|
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||||
|
*/
|
||||||
|
export const chacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20));
|
||||||
|
/**
|
||||||
|
* XChaCha20-Poly1305 extended-nonce chacha.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||||
|
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||||
|
*/
|
||||||
|
export const xchacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 24, tagLength: 16 }, _poly1305_aead(xchacha20));
|
||||||
|
//# sourceMappingURL=chacha.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/chacha.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/chacha.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
12
node_modules/@noble/ciphers/esm/crypto.js
generated
vendored
Normal file
12
node_modules/@noble/ciphers/esm/crypto.js
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const cr = typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
|
||||||
|
export function randomBytes(bytesLength = 32) {
|
||||||
|
if (cr && typeof cr.getRandomValues === 'function')
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
}
|
||||||
|
export function getWebcryptoSubtle() {
|
||||||
|
if (cr && typeof cr.subtle === 'object' && cr.subtle != null)
|
||||||
|
return cr.subtle;
|
||||||
|
throw new Error('crypto.subtle must be defined');
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=crypto.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/crypto.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/crypto.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAGA,MAAM,EAAE,GAAG,OAAO,UAAU,KAAK,QAAQ,IAAI,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAEpG,MAAM,UAAU,WAAW,CAAC,WAAW,GAAG,EAAE;IAC1C,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,eAAe,KAAK,UAAU;QAChD,OAAO,EAAE,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC,MAAM,CAAC;IAC/E,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;AACnD,CAAC"}
|
||||||
17
node_modules/@noble/ciphers/esm/cryptoNode.js
generated
vendored
Normal file
17
node_modules/@noble/ciphers/esm/cryptoNode.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
||||||
|
// See utils.ts for details.
|
||||||
|
// The file will throw on node.js 14 and earlier.
|
||||||
|
// @ts-ignore
|
||||||
|
import * as nc from 'node:crypto';
|
||||||
|
const cr = nc && typeof nc === 'object' && 'webcrypto' in nc ? nc.webcrypto : undefined;
|
||||||
|
export function randomBytes(bytesLength = 32) {
|
||||||
|
if (cr && typeof cr.getRandomValues === 'function')
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
}
|
||||||
|
export function getWebcryptoSubtle() {
|
||||||
|
if (cr && typeof cr.subtle === 'object' && cr.subtle != null)
|
||||||
|
return cr.subtle;
|
||||||
|
throw new Error('crypto.subtle must be defined');
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=cryptoNode.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/cryptoNode.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/cryptoNode.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"cryptoNode.js","sourceRoot":"","sources":["../src/cryptoNode.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,4BAA4B;AAC5B,iDAAiD;AACjD,aAAa;AACb,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,MAAM,EAAE,GAAG,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,WAAW,IAAI,EAAE,CAAC,CAAC,CAAE,EAAE,CAAC,SAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;AAEjG,MAAM,UAAU,WAAW,CAAC,WAAW,GAAG,EAAE;IAC1C,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,eAAe,KAAK,UAAU;QAChD,OAAO,EAAE,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC,MAAM,CAAC;IAC/E,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;AACnD,CAAC"}
|
||||||
149
node_modules/@noble/ciphers/esm/ff1.js
generated
vendored
Normal file
149
node_modules/@noble/ciphers/esm/ff1.js
generated
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { bytesToNumberBE, numberToBytesBE } from './utils.js';
|
||||||
|
import { unsafe } from './aes.js';
|
||||||
|
// NOTE: no point in inlining encrypt instead of encryptBlock, since BigInt stuff will be slow
|
||||||
|
const { expandKeyLE, encryptBlock } = unsafe;
|
||||||
|
// Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
|
||||||
|
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf
|
||||||
|
const BLOCK_LEN = 16;
|
||||||
|
function mod(a, b) {
|
||||||
|
const result = a % b;
|
||||||
|
return result >= 0 ? result : b + result;
|
||||||
|
}
|
||||||
|
function NUMradix(radix, data) {
|
||||||
|
let res = BigInt(0);
|
||||||
|
for (let i of data)
|
||||||
|
res = res * BigInt(radix) + BigInt(i);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
function getRound(radix, key, tweak, x) {
|
||||||
|
if (radix > 2 ** 16 - 1)
|
||||||
|
throw new Error(`Invalid radix: ${radix}`);
|
||||||
|
// radix**minlen ≥ 100
|
||||||
|
const minLen = Math.ceil(Math.log(100) / Math.log(radix));
|
||||||
|
const maxLen = 2 ** 32 - 1;
|
||||||
|
// 2 ≤ minlen ≤ maxlen < 2**32
|
||||||
|
if (2 > minLen || minLen > maxLen || maxLen >= 2 ** 32)
|
||||||
|
throw new Error('Invalid radix: 2 ≤ minlen ≤ maxlen < 2**32');
|
||||||
|
if (x.length < minLen || x.length > maxLen)
|
||||||
|
throw new Error('X is outside minLen..maxLen bounds');
|
||||||
|
const u = Math.floor(x.length / 2);
|
||||||
|
const v = x.length - u;
|
||||||
|
const b = Math.ceil(Math.ceil(v * Math.log2(radix)) / 8);
|
||||||
|
const d = 4 * Math.ceil(b / 4) + 4;
|
||||||
|
const padding = mod(-tweak.length - b - 1, 16);
|
||||||
|
// P = [1]1 || [2]1 || [1]1 || [radix]3 || [10]1 || [u mod 256]1 || [n]4 || [t]4.
|
||||||
|
const P = new Uint8Array([1, 2, 1, 0, 0, 0, 10, u, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||||
|
const view = new DataView(P.buffer);
|
||||||
|
view.setUint16(4, radix, false);
|
||||||
|
view.setUint32(8, x.length, false);
|
||||||
|
view.setUint32(12, tweak.length, false);
|
||||||
|
// Q = T || [0](−t−b−1) mod 16 || [i]1 || [NUMradix(B)]b.
|
||||||
|
const PQ = new Uint8Array(P.length + tweak.length + padding + 1 + b);
|
||||||
|
PQ.set(P);
|
||||||
|
P.fill(0);
|
||||||
|
PQ.set(tweak, P.length);
|
||||||
|
const xk = expandKeyLE(key);
|
||||||
|
const round = (A, B, i, decrypt = false) => {
|
||||||
|
// Q = ... || [i]1 || [NUMradix(B)]b.
|
||||||
|
PQ[PQ.length - b - 1] = i;
|
||||||
|
if (b)
|
||||||
|
PQ.set(numberToBytesBE(NUMradix(radix, B), b), PQ.length - b);
|
||||||
|
// PRF
|
||||||
|
let r = new Uint8Array(16);
|
||||||
|
for (let j = 0; j < PQ.length / BLOCK_LEN; j++) {
|
||||||
|
for (let i = 0; i < BLOCK_LEN; i++)
|
||||||
|
r[i] ^= PQ[j * BLOCK_LEN + i];
|
||||||
|
encryptBlock(xk, r);
|
||||||
|
}
|
||||||
|
// Let S be the first d bytes of the following string of ⎡d/16⎤ blocks:
|
||||||
|
// R || CIPHK(R ⊕[1]16) || CIPHK(R ⊕[2]16) ...CIPHK(R ⊕[⎡d / 16⎤ – 1]16).
|
||||||
|
let s = Array.from(r);
|
||||||
|
for (let j = 1; s.length < d; j++) {
|
||||||
|
const block = numberToBytesBE(BigInt(j), 16);
|
||||||
|
for (let k = 0; k < BLOCK_LEN; k++)
|
||||||
|
block[k] ^= r[k];
|
||||||
|
s.push(...Array.from(encryptBlock(xk, block)));
|
||||||
|
}
|
||||||
|
let y = bytesToNumberBE(Uint8Array.from(s.slice(0, d)));
|
||||||
|
s.fill(0);
|
||||||
|
if (decrypt)
|
||||||
|
y = -y;
|
||||||
|
const m = i % 2 === 0 ? u : v;
|
||||||
|
let c = mod(NUMradix(radix, A) + y, BigInt(radix) ** BigInt(m));
|
||||||
|
// STR(radix, m, c)
|
||||||
|
const C = Array(m).fill(0);
|
||||||
|
for (let i = 0; i < m; i++, c /= BigInt(radix))
|
||||||
|
C[m - 1 - i] = Number(c % BigInt(radix));
|
||||||
|
A.fill(0);
|
||||||
|
A = B;
|
||||||
|
B = C;
|
||||||
|
return [A, B];
|
||||||
|
};
|
||||||
|
const destroy = () => {
|
||||||
|
xk.fill(0);
|
||||||
|
PQ.fill(0);
|
||||||
|
};
|
||||||
|
return { u, round, destroy };
|
||||||
|
}
|
||||||
|
const EMPTY_BUF = new Uint8Array([]);
|
||||||
|
export function FF1(radix, key, tweak = EMPTY_BUF) {
|
||||||
|
const PQ = getRound.bind(null, radix, key, tweak);
|
||||||
|
return {
|
||||||
|
encrypt(x) {
|
||||||
|
const { u, round, destroy } = PQ(x);
|
||||||
|
let [A, B] = [x.slice(0, u), x.slice(u)];
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
[A, B] = round(A, B, i);
|
||||||
|
destroy();
|
||||||
|
const res = A.concat(B);
|
||||||
|
A.fill(0);
|
||||||
|
B.fill(0);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
decrypt(x) {
|
||||||
|
const { u, round, destroy } = PQ(x);
|
||||||
|
// The FF1.Decrypt algorithm is similar to the FF1.Encrypt algorithm;
|
||||||
|
// the differences are in Step 6, where:
|
||||||
|
// 1) the order of the indices is reversed,
|
||||||
|
// 2) the roles of A and B are swapped
|
||||||
|
// 3) modular addition is replaced by modular subtraction, in Step 6vi.
|
||||||
|
let [B, A] = [x.slice(0, u), x.slice(u)];
|
||||||
|
for (let i = 9; i >= 0; i--)
|
||||||
|
[A, B] = round(A, B, i, true);
|
||||||
|
destroy();
|
||||||
|
const res = B.concat(A);
|
||||||
|
A.fill(0);
|
||||||
|
B.fill(0);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Binary string which encodes each byte in little-endian byte order
|
||||||
|
const binLE = {
|
||||||
|
encode(bytes) {
|
||||||
|
const x = [];
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
for (let j = 0, tmp = bytes[i]; j < 8; j++, tmp >>= 1)
|
||||||
|
x.push(tmp & 1);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
},
|
||||||
|
decode(b) {
|
||||||
|
if (b.length % 8)
|
||||||
|
throw new Error('Invalid binary string');
|
||||||
|
const res = new Uint8Array(b.length / 8);
|
||||||
|
for (let i = 0, j = 0; i < res.length; i++) {
|
||||||
|
res[i] = b[j++] | (b[j++] << 1) | (b[j++] << 2) | (b[j++] << 3);
|
||||||
|
res[i] |= (b[j++] << 4) | (b[j++] << 5) | (b[j++] << 6) | (b[j++] << 7);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export function BinaryFF1(key, tweak = EMPTY_BUF) {
|
||||||
|
const ff1 = FF1(2, key, tweak);
|
||||||
|
return {
|
||||||
|
encrypt: (x) => binLE.decode(ff1.encrypt(binLE.encode(x))),
|
||||||
|
decrypt: (x) => binLE.decode(ff1.decrypt(binLE.encode(x))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=ff1.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/ff1.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/ff1.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
node_modules/@noble/ciphers/esm/index.js
generated
vendored
Normal file
3
node_modules/@noble/ciphers/esm/index.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
throw new Error('noble-ciphers have no entry-point: consult README for usage');
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
node_modules/@noble/ciphers/esm/index.js.map
generated
vendored
Normal file
1
node_modules/@noble/ciphers/esm/index.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC"}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user