Monorepo: Gitea CI, docs, auth/sync, backend APIs, gitignore

- Add Gitea Actions workflow; point README to gitea.d-bis.org/Sankofa_Phoenix/SMOA
- Expand .gitignore for Spring H2 data, secrets, Kotlin .kotlin/, tooling
- Track docs/api/generated ReDoc bundle; refresh api docs README
- Android: network/auth/sync, UI shell, tests; backend credentials/integrity APIs
- Docs, scripts (generate-api-docs), modules and core updates

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-23 20:19:24 -07:00
parent f97e23592c
commit a2dc194a49
234 changed files with 10008 additions and 1149 deletions
+4 -1
View File
@@ -15,7 +15,9 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
### Getting Started
- [Project README](../README.md) - Project overview and quick start
- [TODO – Remaining and optional tasks](../TODO.md) - Single checklist for remaining and optional work (backend, Android, iOS, Web, infra, compliance, testing)
- [TODO – Status log](../TODO.md) - What was delivered vs external gates
- [TASKS – Master task list](../TASKS.md) - All tasks by area with completion notes
- [Build (Gradle/Java)](development/BUILD.md) - Commands and CI
- [Specification](reference/SPECIFICATION.md) - Application specification
- [Documentation Recommendations](DOCUMENTATION_RECOMMENDATIONS.md) - Documentation organization recommendations
- [Documentation Plan](standards/DOCUMENTATION_PLAN.md) - Comprehensive documentation plan
@@ -63,6 +65,7 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
### Technical Documentation
- [Architecture Documentation](architecture/) - System and security architecture
- [Menu navigation and REST endpoints](architecture/MENU-AND-ENDPOINTS.md) - Single diagram: drawer, routes, pull/sync `/api/v1`, local caches
- [API Documentation](api/) - API specifications and reference
- [Database Schema](database/) - Database schema and data models
- [Integration Documentation](integrations/) - External system integrations
+6 -4
View File
@@ -10,6 +10,8 @@
This directory contains API documentation for the Secure Mobile Operations Application (SMOA). The API documentation includes OpenAPI specifications, generated documentation, and API reference guides.
**Android client map:** How the in-app **menu / navigation** relates to **`/api/v1` pull and sync** (and local caches) is documented in **[../architecture/MENU-AND-ENDPOINTS.md](../architecture/MENU-AND-ENDPOINTS.md)** (Mermaid diagram).
---
## API Specification
@@ -20,9 +22,9 @@ This directory contains API documentation for the Secure Mobile Operations Appli
- **Status:** In Progress
### Generated Documentation
- **Location:** [generated/](generated/)
- **Format:** HTML (generated from OpenAPI spec)
- **Status:** To be generated
- **Location:** [generated/](generated/) (`index.html` + bundled spec copy)
- **Format:** HTML (ReDoc; regenerate with `bash scripts/generate-api-docs.sh` after editing `api-specification.yaml`)
- **Status:** Tracked in git
---
@@ -223,7 +225,7 @@ val order = apiService.createOrder(orderRequest)
- [OpenAPI Specification](api-specification.yaml)
- [Architecture Documentation](../architecture/ARCHITECTURE.md)
- [Implementation Status](../IMPLEMENTATION_STATUS.md)
- [Implementation Status](../status/IMPLEMENTATION_STATUS.md)
---
+57 -2
View File
@@ -3,7 +3,13 @@ info:
title: SMOA API Specification
description: |
API specification for Secure Mobile Operations Application (SMOA).
This specification documents all internal and external APIs.
**Authoritative mobile sync contract:** the running backend’s OpenAPI document
(`/v3/api-docs` via springdoc) matches Kotlin DTOs in `backend/src/main/kotlin/.../api/dto`.
This YAML is a **design reference**; prefer springdoc export in CI if drift is detected.
Credential **sync** uses `CredentialSyncRequest` (POST `/api/v1/sync/credential`), not the
legacy `Credential` resource shape below where paths differ.
version: 1.0.0
contact:
name: SMOA Development Team
@@ -344,7 +350,23 @@ components:
type: string
type:
type: string
enum: [id, badge, license, permit, other]
description: >
Canonical values align with Complete Credential CredentialRef.domain plus
SMOA barcode templates (see docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md).
enum:
- piv_pki
- payment
- nfc_access
- mobile
- icao9303_mrtd
- aamva_dlid
- mil_std_129
- agency_badge
- id
- badge
- license
- permit
- other
title:
type: string
issuer:
@@ -382,6 +404,39 @@ components:
type: string
format: date
CredentialSyncRequest:
type: object
required:
- credentialId
- holderId
- credentialType
description: >
Mobile sync body for POST /api/v1/sync/credential. credentialType matches
SmoaCredentialType (canonical + legacy). payload is optional JSON validated
per docs/schemas when issuing ICAO/AAMVA/military document types.
properties:
credentialId:
type: string
holderId:
type: string
credentialType:
type: string
pattern: ^(piv_pki|payment|nfc_access|mobile|icao9303_mrtd|aamva_dlid|mil_std_129|agency_badge|id|badge|license|permit|other)$
issuer:
type: string
issuedAt:
type: integer
format: int64
expiresAt:
type: integer
format: int64
payload:
type: object
additionalProperties: true
clientUpdatedAt:
type: integer
format: int64
Order:
type: object
properties:
+524
View File
@@ -0,0 +1,524 @@
openapi: 3.0.3
info:
title: SMOA API Specification
description: |
API specification for Secure Mobile Operations Application (SMOA).
**Authoritative mobile sync contract:** the running backend’s OpenAPI document
(`/v3/api-docs` via springdoc) matches Kotlin DTOs in `backend/src/main/kotlin/.../api/dto`.
This YAML is a **design reference**; prefer springdoc export in CI if drift is detected.
Credential **sync** uses `CredentialSyncRequest` (POST `/api/v1/sync/credential`), not the
legacy `Credential` resource shape below where paths differ.
version: 1.0.0
contact:
name: SMOA Development Team
email: [email protected]
license:
name: Proprietary - Government Use Only
servers:
- url: https://api.smoa.example.com/v1
description: Production server
- url: https://api-dev.smoa.example.com/v1
description: Development server
tags:
- name: Authentication
description: Authentication and authorization endpoints
- name: Credentials
description: Digital credential management
- name: Orders
description: Orders management
- name: Evidence
description: Evidence chain of custody
- name: Reports
description: Report generation
- name: Communications
description: Secure communications
- name: Directory
description: Internal directory
security:
- BearerAuth: []
- ApiKeyAuth: []
paths:
/auth/login:
post:
tags:
- Authentication
summary: Authenticate user
description: |
Authenticate user with multi-factor authentication (PIN + Biometric).
Returns authentication token on success.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Authentication successful
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'401':
description: Authentication failed
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Too many login attempts
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/auth/logout:
post:
tags:
- Authentication
summary: Logout user
description: Invalidates current session
responses:
'200':
description: Logout successful
'401':
description: Unauthorized
/credentials:
get:
tags:
- Credentials
summary: List user credentials
description: Returns list of credentials available to the authenticated user
parameters:
- name: type
in: query
schema:
type: string
enum: [id, badge, license, permit, other]
description: Filter by credential type
responses:
'200':
description: List of credentials
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Credential'
'401':
description: Unauthorized
post:
tags:
- Credentials
summary: Create new credential
description: Creates a new digital credential
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CredentialCreate'
responses:
'201':
description: Credential created
content:
application/json:
schema:
$ref: '#/components/schemas/Credential'
'400':
description: Invalid request
'401':
description: Unauthorized
/credentials/{id}:
get:
tags:
- Credentials
summary: Get credential by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Credential ID
responses:
'200':
description: Credential details
content:
application/json:
schema:
$ref: '#/components/schemas/Credential'
'404':
description: Credential not found
'401':
description: Unauthorized
/orders:
get:
tags:
- Orders
summary: List orders
description: Returns list of orders available to the authenticated user
parameters:
- name: status
in: query
schema:
type: string
enum: [draft, pending_approval, approved, issued, executed, expired, revoked]
description: Filter by order status
- name: type
in: query
schema:
type: string
enum: [authorization, assignment, search_warrant, arrest_warrant, court_order, administrative]
description: Filter by order type
responses:
'200':
description: List of orders
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
'401':
description: Unauthorized
post:
tags:
- Orders
summary: Create new order
description: Creates a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCreate'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid request
'401':
description: Unauthorized
/orders/{id}:
get:
tags:
- Orders
summary: Get order by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Order ID
responses:
'200':
description: Order details
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
'401':
description: Unauthorized
/evidence:
get:
tags:
- Evidence
summary: List evidence items
description: Returns list of evidence items
responses:
'200':
description: List of evidence items
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Evidence'
'401':
description: Unauthorized
/reports:
post:
tags:
- Reports
summary: Generate report
description: Generates a report in the specified format
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReportRequest'
responses:
'200':
description: Report generated
content:
application/pdf:
schema:
type: string
format: binary
application/json:
schema:
type: string
application/xml:
schema:
type: string
text/csv:
schema:
type: string
'400':
description: Invalid request
'401':
description: Unauthorized
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
schemas:
LoginRequest:
type: object
required:
- pin
- biometricToken
properties:
pin:
type: string
description: User PIN
minLength: 6
maxLength: 12
biometricToken:
type: string
description: Biometric authentication token
LoginResponse:
type: object
properties:
token:
type: string
description: Authentication token
expiresIn:
type: integer
description: Token expiration time in seconds
user:
$ref: '#/components/schemas/User'
User:
type: object
properties:
id:
type: string
username:
type: string
roles:
type: array
items:
type: string
Credential:
type: object
properties:
id:
type: string
type:
type: string
description: >
Canonical values align with Complete Credential CredentialRef.domain plus
SMOA barcode templates (see docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md).
enum:
- piv_pki
- payment
- nfc_access
- mobile
- icao9303_mrtd
- aamva_dlid
- mil_std_129
- agency_badge
- id
- badge
- license
- permit
- other
title:
type: string
issuer:
type: string
issueDate:
type: string
format: date
expirationDate:
type: string
format: date
status:
type: string
enum: [active, expired, revoked]
barcode:
type: string
description: PDF417 barcode data
CredentialCreate:
type: object
required:
- type
- title
- issuer
properties:
type:
type: string
title:
type: string
issuer:
type: string
issueDate:
type: string
format: date
expirationDate:
type: string
format: date
CredentialSyncRequest:
type: object
required:
- credentialId
- holderId
- credentialType
description: >
Mobile sync body for POST /api/v1/sync/credential. credentialType matches
SmoaCredentialType (canonical + legacy). payload is optional JSON validated
per docs/schemas when issuing ICAO/AAMVA/military document types.
properties:
credentialId:
type: string
holderId:
type: string
credentialType:
type: string
pattern: ^(piv_pki|payment|nfc_access|mobile|icao9303_mrtd|aamva_dlid|mil_std_129|agency_badge|id|badge|license|permit|other)$
issuer:
type: string
issuedAt:
type: integer
format: int64
expiresAt:
type: integer
format: int64
payload:
type: object
additionalProperties: true
clientUpdatedAt:
type: integer
format: int64
Order:
type: object
properties:
id:
type: string
type:
type: string
enum: [authorization, assignment, search_warrant, arrest_warrant, court_order, administrative]
title:
type: string
status:
type: string
enum: [draft, pending_approval, approved, issued, executed, expired, revoked]
issuedBy:
type: string
issueDate:
type: string
format: date-time
expirationDate:
type: string
format: date-time
OrderCreate:
type: object
required:
- type
- title
properties:
type:
type: string
title:
type: string
content:
type: string
expirationDate:
type: string
format: date-time
Evidence:
type: object
properties:
id:
type: string
caseNumber:
type: string
description:
type: string
type:
type: string
enum: [physical, digital, biological, chemical, firearm, document]
collectionDate:
type: string
format: date-time
currentCustodian:
type: string
ReportRequest:
type: object
required:
- template
- format
properties:
template:
type: string
description: Report template name
format:
type: string
enum: [pdf, xml, json, csv]
parameters:
type: object
description: Template parameters
ErrorResponse:
type: object
properties:
error:
type: string
message:
type: string
code:
type: string
timestamp:
type: string
format: date-time
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SMOA API — ReDoc</title>
<style>body { margin: 0; }</style>
</head>
<body>
<redoc spec-url="./api-specification.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>
+9 -5
View File
@@ -110,6 +110,10 @@ SMOA operates in a secure mobile and multi-platform environment with:
12. **modules:judicial** - Judicial operations
13. **modules:intelligence** - Intelligence operations
### Client navigation and API surface (Android)
For a **single diagram** that merges the **app menu (drawer / NavHost)** with **GET pull** and **POST/DELETE sync** endpoints under `/api/v1`, see **[MENU-AND-ENDPOINTS.md](MENU-AND-ENDPOINTS.md)**.
---
## Component Architecture
@@ -240,7 +244,7 @@ Each module follows a consistent structure:
## Security Architecture
See [Security Architecture Document](SECURITY_ARCHITECTURE.md) for detailed security architecture.
See [Security Architecture Document](../security/SMOA-Security-Architecture.md) for detailed security architecture.
### Key Security Features
- Multi-factor authentication
@@ -301,10 +305,10 @@ See [Security Architecture Document](SECURITY_ARCHITECTURE.md) for detailed secu
## References
- [Specification](../SPECIFICATION.md)
- [Security Architecture](SECURITY_ARCHITECTURE.md)
- [Implementation Status](../IMPLEMENTATION_STATUS.md)
- [Compliance Matrix](../COMPLIANCE_MATRIX.md)
- [Specification](../reference/SPECIFICATION.md)
- [Security Architecture](../security/SMOA-Security-Architecture.md)
- [Implementation Status](../status/IMPLEMENTATION_STATUS.md)
- [Compliance Matrix](../reference/COMPLIANCE_MATRIX.md)
---
+134
View File
@@ -0,0 +1,134 @@
# Android app: menu navigation and REST endpoints
**Purpose:** One view of how the **drawer / NavHost** (user-visible “menu”) relates to **hosted config**, **pull GETs**, **sync POST/DELETE**, and **local caches**.
**Scope:** Android app (`app`, `core/common` sync, Retrofit services) and backend **`/api/v1`** contract.
---
## Single diagram (Mermaid)
Render this in any Mermaid-capable viewer (GitHub, GitLab, many IDEs, Notion, etc.).
```mermaid
flowchart TB
subgraph MENU["From menu — Drawer / Home → NavHost"]
direction TB
HM[("☰ Menu / Home hub")]
HM --> R0["route: home — startDestination"]
HM --> R1["route: credentials — always in menu"]
HM --> R6["route: orders — always in menu"]
HM --> R7["route: evidence — always in menu"]
HM --> R8["route: reports — always in menu"]
HM --> R2["route: directory — RBAC DIRECTORY"]
HM --> R3["route: communications — RBAC COMMUNICATIONS"]
HM --> R4["route: meetings — RBAC MEETINGS"]
HM --> R5["route: browser — RBAC BROWSER"]
HM --> R9["route: user_settings — always"]
R0 --> S0[HomeScreen — module shortcuts + launcher settings]
R1 --> S1[CredentialsModule — template Cards]
R6 --> S6[OrdersModule]
R7 --> S7[EvidenceModule]
R8 --> S8[ReportGenerationScreen]
R2 --> S2[DirectoryModule — DirectoryListScreen]
R3 --> S3[CommunicationsModule]
R4 --> S4[MeetingsModule]
R5 --> S5[BrowserModule]
R9 --> S9[UserSettingsScreen — account + log out]
end
subgraph NET["Shared networking — not one endpoint per menu row"]
direction TB
CFG["GET hosted client-config JSON\n(SMOA_CONFIG_URL / ClientConfigFetcher)"]
RS[(RemoteEndpointStore +\nRetrofit base URL + API key)]
SS[SyncService.startSync\nwhen online and Backend Pull/Sync active]
CFG --> RS
RS --> SS
SS --> PULL[Pull phase — all resource types]
SS --> PUSH[Queue phase — POST/DELETE sync items]
end
subgraph API["Backend — same host /api/v1"]
direction TB
subgraph PULL_EP["Pull"]
G1[GET /directory]
G2[GET /orders]
G3[GET /evidence]
G4[GET /credentials]
G5[GET /reports]
end
subgraph SYNC_EP["Sync"]
P1[POST /sync/directory]
P2[POST /sync/order]
P3[POST /sync/evidence]
P4[POST /sync/credential]
P5[POST /sync/report]
D1[DELETE /sync/directory/{id}]
D2[DELETE /sync/order/{id}]
D3[DELETE /sync/evidence/{id}]
D4[DELETE /sync/credential/{id}]
D5[DELETE /sync/report/{id}]
end
end
subgraph LOCAL["Local persistence"]
CC[("Credential Room cache")]
SN[("sync_conflict_snapshots\norder / evidence / directory / report")]
end
PULL --> G1
PULL --> G2
PULL --> G3
PULL --> G4
PULL --> G5
PUSH --> P1
PUSH --> P2
PUSH --> P3
PUSH --> P4
PUSH --> P5
PUSH --> D1
PUSH --> D2
PUSH --> D3
PUSH --> D4
PUSH --> D5
G4 -->|merge pull JSON| CC
P4 -->|success upsert| CC
D4 -->|remove row| CC
PUSH --> CR[Conflict + remoteData]
CR -->|credential| CC
CR -->|other resource types| SN
```
---
## Reading the diagram
| Area | Meaning |
|------|--------|
| **MENU** | `NavigationDrawer` + `HomeScreen` + `SMOANavigation` (`SMOARoute`). **Start:** `home`. **Always listed:** credentials, orders, evidence, reports, user settings. **RBAC-gated:** directory, communications, meetings, browser. |
| **NET** | **Hosted config** is optional; when present it updates **RemoteEndpointStore** and thus Retrofit’s base URL. **`SyncService.startSync`** runs **pull** (all listed GETs) then drains the **outbound sync queue** (POST/DELETE). Individual screens do not own dedicated REST calls for that batch. |
| **API** | Matches **`BackendPullApiService`** / **`BackendSyncApiService`** and Spring **`PullController`** / **`SyncController`**. Headers such as **`X-API-Key`** and **`X-Unit`** are as implemented in the app. |
| **LOCAL** | **Credential cache:** pull merge, successful credential sync, conflict **`remoteData`**, **UseLocal** restore, delete. **Snapshots table:** raw JSON for **non-credential** conflicts only (`SyncConflictSnapshotStore` / `SyncConflictSnapshotRepository`). |
---
## Related documentation
| Topic | Location |
|--------|-----------|
| Backend sync/delete and audit | [backend/README.md](../../backend/README.md) |
| OpenAPI / drift | [development/OPENAPI-SYNCHRONIZATION.md](../development/OPENAPI-SYNCHRONIZATION.md) |
| OpenAPI artifact | [api/api-specification.yaml](../api/api-specification.yaml) |
| Room / `credential_cache` | [database/DATABASE_SCHEMA.md](../database/DATABASE_SCHEMA.md) |
| Frontend–backend contract | [reference/REQUIREMENTS-ALIGNMENT.md](../reference/REQUIREMENTS-ALIGNMENT.md) |
---
## Source references (implementation)
- Menu and routes: `app/.../ui/navigation/NavigationDrawer.kt`, `NavigationModule.kt` (`SMOARoute`, `SMOANavigation`).
- Shell: `app/.../ui/main/MainScreen.kt`.
- Retrofit: `app/.../api/BackendPullApiService.kt`, `BackendSyncApiService.kt`, `SyncRetrofitHolder.kt`.
- Pull/sync orchestration: `core/common/.../SyncService.kt`.
- Backend: `backend/.../api/PullController.kt`, `SyncController.kt`.
@@ -49,8 +49,8 @@
|--------|--------|-----------------|-------------------|
| core:barcode | ✅ Complete | 2024-02-15 | [core-barcode-completion-report.md](../modules/core-barcode-completion-report.md) |
| modules:orders | ✅ Complete | 2024-02-28 | [modules-orders-completion-report.md](../modules/modules-orders-completion-report.md) |
| modules:evidence | ✅ Complete | 2024-03-15 | [modules-evidence-completion-report.md](../modules/modules-evidence-completion-report.md) |
| modules:reports | ✅ Complete | 2024-03-25 | [modules-reports-completion-report.md](../modules/modules-reports-completion-report.md) |
| modules:evidence | ✅ Complete | 2024-03-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
| modules:reports | ✅ Complete | 2024-03-25 | [completion/modules](../modules/) *(no dedicated report file yet)* |
### Module Completion Statistics
- **Total Modules:** 4
@@ -47,11 +47,11 @@
### Modules in This Phase
| Module | Status | Completion Date | Completion Report |
|--------|--------|-----------------|-------------------|
| modules:atf | ✅ Complete | 2024-06-15 | [modules-atf-completion-report.md](../modules/modules-atf-completion-report.md) |
| modules:ncic | ✅ Complete | 2024-07-30 | [modules-ncic-completion-report.md](../modules/modules-ncic-completion-report.md) |
| modules:military | ✅ Complete | 2024-08-15 | [modules-military-completion-report.md](../modules/modules-military-completion-report.md) |
| modules:judicial | ✅ Complete | 2024-09-15 | [modules-judicial-completion-report.md](../modules/modules-judicial-completion-report.md) |
| modules:intelligence | ✅ Complete | 2024-09-30 | [modules-intelligence-completion-report.md](../modules/modules-intelligence-completion-report.md) |
| modules:atf | ✅ Complete | 2024-06-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
| modules:ncic | ✅ Complete | 2024-07-30 | [completion/modules](../modules/) *(no dedicated report file yet)* |
| modules:military | ✅ Complete | 2024-08-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
| modules:judicial | ✅ Complete | 2024-09-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
| modules:intelligence | ✅ Complete | 2024-09-30 | [completion/modules](../modules/) *(no dedicated report file yet)* |
---
+203 -280
View File
@@ -1,298 +1,221 @@
# SMOA Database Schema Documentation
# SMOA database schema (server + device)
**Version:** 1.0
**Last Updated:** 2024-12-20
**Status:** Draft - In Progress
**Version:** 2.0
**Last updated:** 2026-03-23
This document aligns **SMOA backend** (Flyway `V1__baseline.sql`, H2 or PostgreSQL) with **on-device Room** entities in this repository. Older rows that described only a generic local `credentials` table **without** a matching entity are removed.
---
## Database Overview
## 1. Server database (SMOA backend)
### Database Technology
- **Database:** SQLite (via Room)
- **Version:** SQLite 3.x
- **Location:** Local device storage
- **Encryption:** AES-256-GCM encryption
Source of truth: `backend/src/main/resources/db/migration/V1__baseline.sql`.
JPA entities: `backend/src/main/kotlin/com/smoa/backend/domain/*.kt`.
### Database Purpose
SMOA uses Room database for local data storage, providing:
- Offline data access
- Fast local queries
- Encrypted data storage
- Data synchronization support
### 1.1 `directory_entries`
| Column | SQL type | Description |
|--------|-----------|-------------|
| id | VARCHAR(255) PK | Directory entry id |
| name | VARCHAR(255) | Display name |
| title | VARCHAR(255) | Job title |
| unit | VARCHAR(255) | Unit / org |
| phone_number | VARCHAR(255) | Phone |
| extension | VARCHAR(255) | Extension |
| email | VARCHAR(255) | Email |
| secure_routing_id | VARCHAR(255) | Secure routing |
| role | VARCHAR(255) | Role |
| clearance_level | VARCHAR(255) | Clearance |
| last_updated | BIGINT | Epoch ms |
### 1.2 `orders`
| Column | SQL type | Description |
|--------|-----------|-------------|
| order_id | VARCHAR(255) PK | Order id |
| order_type | VARCHAR(255) | Enum string (see backend validation) |
| title | VARCHAR(255) | Title |
| content | CLOB | Body |
| issued_by | VARCHAR(255) | Issuer |
| issued_to | VARCHAR(255) | Recipient |
| issue_date | TIMESTAMP | Issued |
| effective_date | TIMESTAMP | Effective |
| expiration_date | TIMESTAMP | Expiry |
| status | VARCHAR(255) | Status enum |
| classification | VARCHAR(255) | Classification |
| jurisdiction | VARCHAR(255) | Jurisdiction |
| case_number | VARCHAR(255) | Case |
| updated_at | BIGINT | Epoch ms |
### 1.3 `evidence`
| Column | SQL type | Description |
|--------|-----------|-------------|
| evidence_id | VARCHAR(255) PK | Evidence id |
| case_number | VARCHAR(255) | Case |
| description | CLOB | Description |
| evidence_type | VARCHAR(255) | Type enum |
| collection_date | TIMESTAMP | Collected |
| collection_location | VARCHAR(255) | Location |
| collection_method | VARCHAR(255) | Method |
| collected_by | VARCHAR(255) | Collector |
| current_custodian | VARCHAR(255) | Custodian |
| storage_location | VARCHAR(255) | Storage |
| updated_at | BIGINT | Epoch ms |
### 1.4 `credentials` (server)
| Column | SQL type | Description |
|--------|-----------|-------------|
| credential_id | VARCHAR(255) PK | Credential id |
| holder_id | VARCHAR(255) | Subject / holder |
| credential_type | VARCHAR(255) | Canonical type (`SmoaCredentialType` + legacy; see `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`) |
| issuer | VARCHAR(255) | Issuing authority label |
| issued_at | BIGINT | Epoch ms (optional) |
| expires_at | BIGINT | Epoch ms (optional) |
| payload_json | CLOB | JSON object: format-specific fields, optional `issuingAuthority`, `completeCredentialDomain`, `extensions` |
| updated_at | BIGINT | Epoch ms |
**Issuance validation:** Parse `payload_json` as JSON, then validate with `docs/schemas/credential-payload-*.schema.json` for the given `credential_type` on the sync **request body** (not duplicated inside payload).
### 1.5 `reports`
| Column | SQL type | Description |
|--------|-----------|-------------|
| report_id | VARCHAR(255) PK | Report id |
| report_type | VARCHAR(255) | Type enum |
| title | VARCHAR(255) | Title |
| format | VARCHAR(255) | PDF, XML, JSON, CSV, EXCEL |
| generated_date | BIGINT | Epoch ms |
| generated_by | VARCHAR(255) | Generator |
| content | BLOB | Optional binary |
| metadata_json | CLOB | Optional JSON |
| updated_at | BIGINT | Epoch ms |
### 1.6 `sync_audit_log`
| Column | SQL type | Description |
|--------|-----------|-------------|
| id | BIGINT IDENTITY PK | Surrogate key |
| resource_type | VARCHAR(255) | Resource |
| resource_id | VARCHAR(255) | Id |
| operation | VARCHAR(255) | Operation |
| success | BOOLEAN | Outcome |
| principal | VARCHAR(255) | Optional actor |
| timestamp | TIMESTAMP | Event time |
---
## Schema Diagrams
## 2. On-device Room (implemented modules)
### Entity Relationship Diagram
[To be added: ER diagram showing all entities and relationships]
Room uses SQLite; module-specific databases. Below match Kotlin `@Entity` types.
### 2.1 Directory — `directory_entries`
`modules/directory/.../DirectoryEntity.kt`
| Column | Type | Notes |
|--------|------|--------|
| id | TEXT PK | |
| name | TEXT | |
| title | TEXT? | |
| unit | TEXT | |
| phoneNumber | TEXT? | camelCase in Kotlin → snake in DB per Room |
| extension | TEXT? | |
| email | TEXT? | |
| secureRoutingId | TEXT? | |
| role | TEXT? | |
| clearanceLevel | TEXT? | |
| lastUpdated | INTEGER | Epoch ms |
### 2.2 Orders — `orders`
`modules/orders/.../OrderEntity.kt`
| Column | Type | Notes |
|--------|------|--------|
| orderId | TEXT PK | |
| orderType | TEXT | Enum persisted via converter |
| title | TEXT | |
| content | TEXT | |
| issuedBy | TEXT | |
| issuedTo | TEXT? | |
| issueDate | INTEGER / Date | Type converters |
| effectiveDate | | |
| expirationDate | | |
| status | | Enum |
| classification | TEXT? | |
| jurisdiction | TEXT | |
| caseNumber | TEXT? | |
| createdAt | | |
| updatedAt | | |
### 2.3 Evidence — `evidence`
`modules/evidence/.../EvidenceEntity.kt`
| Column | Type | Notes |
|--------|------|--------|
| evidenceId | TEXT PK | |
| caseNumber | TEXT | |
| description | TEXT | |
| evidenceType | | Enum |
| collectionDate | | |
| collectionLocation | TEXT | |
| collectionMethod | TEXT | |
| collectedBy | TEXT | |
| currentCustodian | TEXT | |
| storageLocation | TEXT? | |
| createdAt | | |
| updatedAt | | |
### 2.4 Custody — `custody_transfers`
`modules/evidence/.../CustodyTransferEntity.kt`
| Column | Type | Notes |
|--------|------|--------|
| transferId | TEXT PK | |
| evidenceId | TEXT FK → evidence | |
| timestamp | Date | |
| fromCustodian | TEXT | |
| toCustodian | TEXT | |
| reason | TEXT | |
| evidenceCondition | TEXT | |
| signatureData | BLOB? | |
| notes | TEXT? | |
### 2.5 Credentials cache — `credential_cache`
`modules/credentials/.../CredentialCacheEntity.kt` (encrypted Room DB `credential_cache_database`)
| Column | Type | Notes |
|--------|------|--------|
| credentialId | TEXT PK | |
| holderId | TEXT | |
| credentialType | TEXT | |
| issuer | TEXT? | |
| issuedAt | INTEGER? | Epoch ms |
| expiresAt | INTEGER? | Epoch ms |
| payloadJson | TEXT? | JSON string |
| updatedAt | INTEGER | Epoch ms |
Populate from pull/sync merge logic in application code when wired; schema matches server `credentials` for offline display.
### 2.6 Reports (device)
No Room `ReportEntity` in this repo; reports remain server-backed unless a local entity is added later.
---
## Tables
## 3. Indexes (server)
### User Table
#### Table: users
- **Purpose:** Store user information
- **Primary Key:** user_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| user_id | TEXT | PRIMARY KEY | Unique user identifier |
| username | TEXT | NOT NULL, UNIQUE | Username |
| email | TEXT | | Email address |
| role | TEXT | NOT NULL | User role |
| created_at | INTEGER | NOT NULL | Creation timestamp |
| updated_at | INTEGER | NOT NULL | Update timestamp |
### Credential Table
#### Table: credentials
- **Purpose:** Store digital credentials
- **Primary Key:** credential_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| credential_id | TEXT | PRIMARY KEY | Unique credential identifier |
| user_id | TEXT | NOT NULL, FOREIGN KEY | User who owns credential |
| type | TEXT | NOT NULL | Credential type |
| title | TEXT | NOT NULL | Credential title |
| issuer | TEXT | NOT NULL | Issuing authority |
| issue_date | INTEGER | | Issue date (Unix timestamp) |
| expiration_date | INTEGER | | Expiration date |
| status | TEXT | NOT NULL | Status (active, expired, revoked) |
| barcode_data | TEXT | | PDF417 barcode data |
| created_at | INTEGER | NOT NULL | Creation timestamp |
| updated_at | INTEGER | NOT NULL | Update timestamp |
**Foreign Keys:**
- user_id → users(user_id)
### Order Table
#### Table: orders
- **Purpose:** Store digital orders
- **Primary Key:** order_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| order_id | TEXT | PRIMARY KEY | Unique order identifier |
| order_type | TEXT | NOT NULL | Order type |
| title | TEXT | NOT NULL | Order title |
| content | TEXT | NOT NULL | Order content |
| issued_by | TEXT | NOT NULL | Issuing authority |
| issued_to | TEXT | | Recipient |
| issue_date | INTEGER | NOT NULL | Issue date |
| effective_date | INTEGER | NOT NULL | Effective date |
| expiration_date | INTEGER | | Expiration date |
| status | TEXT | NOT NULL | Order status |
| created_at | INTEGER | NOT NULL | Creation timestamp |
| updated_at | INTEGER | NOT NULL | Update timestamp |
### Evidence Table
#### Table: evidence
- **Purpose:** Store evidence items
- **Primary Key:** evidence_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| evidence_id | TEXT | PRIMARY KEY | Unique evidence identifier |
| case_number | TEXT | NOT NULL | Case number |
| description | TEXT | NOT NULL | Evidence description |
| type | TEXT | NOT NULL | Evidence type |
| collection_date | INTEGER | NOT NULL | Collection date |
| collection_location | TEXT | | Collection location |
| collected_by | TEXT | NOT NULL | Collector |
| current_custodian | TEXT | NOT NULL | Current custodian |
| storage_location | TEXT | | Storage location |
| created_at | INTEGER | NOT NULL | Creation timestamp |
| updated_at | INTEGER | NOT NULL | Update timestamp |
### Custody Transfer Table
#### Table: custody_transfers
- **Purpose:** Track evidence custody transfers
- **Primary Key:** transfer_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| transfer_id | TEXT | PRIMARY KEY | Unique transfer identifier |
| evidence_id | TEXT | NOT NULL, FOREIGN KEY | Evidence item |
| from_custodian | TEXT | NOT NULL | Transferring custodian |
| to_custodian | TEXT | NOT NULL | Receiving custodian |
| transfer_date | INTEGER | NOT NULL | Transfer date |
| reason | TEXT | | Transfer reason |
| evidence_condition | TEXT | | Evidence condition |
| signature | TEXT | | Digital signature |
| created_at | INTEGER | NOT NULL | Creation timestamp |
**Foreign Keys:**
- evidence_id → evidence(evidence_id)
### Report Table
#### Table: reports
- **Purpose:** Store generated reports
- **Primary Key:** report_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| report_id | TEXT | PRIMARY KEY | Unique report identifier |
| template | TEXT | NOT NULL | Report template |
| format | TEXT | NOT NULL | Report format (PDF, XML, JSON, CSV) |
| parameters | TEXT | | Report parameters (JSON) |
| generated_by | TEXT | NOT NULL | Generator user |
| generated_at | INTEGER | NOT NULL | Generation timestamp |
| file_path | TEXT | | Report file path |
| file_size | INTEGER | | File size in bytes |
### Audit Log Table
#### Table: audit_logs
- **Purpose:** Store audit trail records
- **Primary Key:** log_id
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| log_id | TEXT | PRIMARY KEY | Unique log identifier |
| event_type | TEXT | NOT NULL | Event type |
| user_id | TEXT | | User who triggered event |
| module | TEXT | | Module where event occurred |
| action | TEXT | NOT NULL | Action performed |
| resource | TEXT | | Resource affected |
| result | TEXT | NOT NULL | Result (success, failure) |
| details | TEXT | | Additional details (JSON) |
| timestamp | INTEGER | NOT NULL | Event timestamp |
| ip_address | TEXT | | IP address (if applicable) |
- `idx_sync_audit_timestamp` on `sync_audit_log(timestamp)`
---
## Indexes
### Performance Indexes
- **users(username):** Index on username for login
- **credentials(user_id):** Index on user_id for user credential queries
- **credentials(status):** Index on status for status queries
- **orders(status):** Index on order status
- **orders(order_type):** Index on order type
- **evidence(case_number):** Index on case number
- **audit_logs(timestamp):** Index on timestamp for time-based queries
- **audit_logs(user_id):** Index on user_id for user audit queries
---
## Data Dictionary
### Data Elements
#### User Data Elements
- **user_id:** Unique identifier for users
- **username:** User login name
- **role:** User role (administrator, operator, viewer, auditor)
#### Credential Data Elements
- **credential_id:** Unique identifier for credentials
- **type:** Credential type (id, badge, license, permit, other)
- **status:** Credential status (active, expired, revoked)
#### Order Data Elements
- **order_id:** Unique identifier for orders
- **order_type:** Order type (authorization, assignment, search_warrant, etc.)
- **status:** Order status (draft, pending_approval, approved, issued, etc.)
#### Evidence Data Elements
- **evidence_id:** Unique identifier for evidence
- **type:** Evidence type (physical, digital, biological, chemical, firearm, document)
- **current_custodian:** Current custodian of evidence
---
## Migrations
### Migration History
#### Migration 1: Initial Schema
- **Version:** 1
- **Date:** 2024-01-01
- **Description:** Initial database schema creation
#### Migration 2: Add Audit Logging
- **Version:** 2
- **Date:** 2024-02-01
- **Description:** Add audit log table and indexes
### Migration Procedures
#### Applying Migrations
1. **Backup Database:** Backup current database
2. **Review Migration:** Review migration script
3. **Test Migration:** Test migration in staging
4. **Apply Migration:** Apply migration to production
5. **Verify Migration:** Verify migration success
#### Rollback Procedures
1. **Identify Migration:** Identify migration to rollback
2. **Backup Current:** Backup current database
3. **Restore Previous:** Restore previous database version
4. **Verify Rollback:** Verify rollback success
---
## Data Protection
### Encryption
- **At Rest:** AES-256-GCM encryption
- **Key Storage:** Hardware-backed key storage
- **Key Management:** Automatic key rotation
### Access Control
- **Database Access:** Application-only access
- **User Access:** Role-based data access
- **Audit Logging:** All access logged
---
## Backup and Recovery
### Backup Procedures
- **Automated Backups:** Daily automated backups
- **Backup Location:** Encrypted backup storage
- **Backup Retention:** 90 days
### Recovery Procedures
- **Full Recovery:** Complete database restoration
- **Partial Recovery:** Selective data restoration
- **Point-in-Time Recovery:** Recovery to specific point
---
## Performance Optimization
### Query Optimization
- **Indexes:** Strategic index placement
- **Query Tuning:** Optimized queries
- **Caching:** Query result caching
### Database Maintenance
- **Vacuum:** Regular database vacuum
- **Analyze:** Regular statistics update
- **Optimization:** Periodic optimization
---
## References
- [Architecture Documentation](../architecture/ARCHITECTURE.md)
- [Administrator Guide](../admin/SMOA-Administrator-Guide.md)
- [Backup and Recovery Procedures](../operations/SMOA-Backup-Recovery-Procedures.md)
---
**Document Owner:** Database Administrator
**Last Updated:** 2024-12-20
**Status:** Draft - In Progress
**Next Review:** 2024-12-27
## 4. References
- Backend Flyway: `backend/src/main/resources/db/migration/V1__baseline.sql`
- Identity / payload schemas: `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`, `docs/schemas/`
- Tenant model: `docs/security/TENANT-THREAT-MODEL.md`
+28
View File
@@ -0,0 +1,28 @@
# Building SMOA
## Requirements
- **JDK 17** (`JAVA_HOME` pointing at a JDK with `bin/java`)
- **Android SDK** with **Platform 34** (match `AppConfig.compileSdk`) for `:app:assembleDebug`
## Commands
```bash
# Recommended one-liner (backend tests + debug APK):
./gradlew smoaVerify --no-daemon
# Or from repo root:
./scripts/build-all.sh
# Individual targets:
./gradlew :backend:test # Spring Boot unit/integration tests
./gradlew :app:assembleDebug # Android debug APK
./gradlew build # Full multi-project (needs Android SDK)
```
Debug APK output: `app/build/outputs/apk/debug/app-debug.apk`
## CI
Gitea Actions runs `./gradlew smoaVerify --no-daemon` on push and pull requests
(see `.gitea/workflows/ci.yml`).
@@ -0,0 +1,22 @@
# Keeping OpenAPI in sync
## Source of truth
The **running Spring Boot app** exposes the authoritative contract:
- **OpenAPI JSON:** `GET /v3/api-docs`
- **Swagger UI:** `/swagger-ui.html`
The static file `docs/api/api-specification.yaml` is a **human-maintained reference** for design reviews. It can drift from springdoc.
## Optional CI drift check
1. Start the backend locally: `./gradlew :backend:bootRun` (or run the JAR).
2. Export: `curl -s http://localhost:8080/v3/api-docs -o /tmp/smoa-openapi.json`
3. Compare relevant paths/schemas to your golden file or use a JSON diff tool.
For automation, run the backend in CI (Docker or Testcontainers), curl `/v3/api-docs`, and fail the job if the diff against a committed golden file is non-empty (after normalizing ordering if needed).
## Script
See `scripts/export-openapi-local.sh` for a minimal local export (requires a reachable backend).
+41
View File
@@ -0,0 +1,41 @@
# Enterprise security configuration (SMOA Android)
Build-time Gradle properties (`-P` or `gradle.properties`) map to `BuildConfig` and runtime behavior.
## TLS certificate pinning
1. Obtain SPKI SHA-256 pins for your API host (e.g. `openssl s_client -connect host:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64` → prefix with `sha256/`).
2. Set **backend base URL** so the host can be resolved: `-Psmoa.backend.baseUrl=https://api.example.com/`
3. Set pins: `-Psmoa.tls.pins=sha256/PRIMARY==,sha256/BACKUP==`
Pinning applies only when `SMOA_BACKEND_BASE_URL` yields a host. Config-only deployments should still set a canonical backend URL for pinning, or extend `NetworkPinningConfig` to read `RemoteEndpointStore`.
## OIDC / OAuth
Set issuer, client id, and redirect URI matching your IdP and app manifest intent-filter:
- `smoa.oidc.issuer`
- `smoa.oidc.clientId`
- `smoa.oidc.redirectUri`
Tokens are stored in **`SecureTokenStore`** (encrypted). **`AuthTokenInterceptor`** adds `Authorization: Bearer` when an access token exists. Wire **AppAuth** or your SSO WebView** to call `SecureTokenStore.persistTokens(...)` after code exchange.
## Session lock
`smoa.session.timeoutMinutes` (default **15**). Set to **0** to disable background lock. Unlock uses the same biometric flow as sign-in (`SessionLockOverlay`).
## Play Integrity
Set `smoa.playIntegrity.cloudProjectNumber` to your Google Cloud project number linked in Play Console. Use **User settings → Run Play Integrity** for a smoke test; verify tokens on your backend with Google’s API.
## Classification label
`smoa.classification.buildMarking` is shown in User settings and should match your security office’s build marking policy (not a substitute for data labeling in content).
## Knox / MDM
`KnoxEnterpriseProbe` only detects Knox classes on the classpath. For enforcement, integrate **Samsung Knox SDK** or your **UEM** (VMware Workspace ONE, Intune, etc.) per deployment standards.
## Biometric-gated AES key
`BiometricSecretsVault` creates a **user-authentication-required** AES key in AndroidKeyStore for wrapping secrets. Complete cipher + `BiometricPrompt.CryptoObject` wiring when binding refresh-token protection to your IdP flow.
+29
View File
@@ -0,0 +1,29 @@
# TURN and signaling for WebRTC
For meetings and communications beyond STUN-only NAT traversal:
## Coturn (TURN/STUN)
Example `turnserver.conf`:
```conf
listening-port=3478
tls-listening-port=5349
realm=smoa.example.com
server-name=smoa.example.com
use-auth-secret
static-auth-secret=YOUR_LONG_SECRET
cert=/etc/letsencrypt/live/smoa.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/smoa.example.com/privkey.pem
```
Issue **time-limited credentials** (HMAC) from your backend; Android app passes them via `InfrastructureManager.setTurnEndpoints`.
## Signaling
Use **WebSocket** or **HTTPS long-poll** between clients and a small Node/Go/Java service that maps room IDs to SDP/ICE relay. Set `SMOA_SIGNALING_URLS` (comma-separated) in the Android build for `InfrastructureManager`.
## Related
- [nginx-smoa.conf.example](./nginx-smoa.conf.example) – TLS termination
- [docker-compose.yml](../../docker-compose.yml) – backend container
+8 -6
View File
@@ -12,12 +12,13 @@ This folder is a **scaffold** for the SMOA iOS app. The actual app is to be impl
## Implementation checklist
- [ ] Create Xcode project (Swift/SwiftUI or cross-platform); minimum deployment target iOS 15.0.
- [ ] Store API key in **Keychain**.
- [ ] Implement **PullAPI** (URLSession or Alamofire): GET endpoints above.
- [ ] Implement **SyncAPI**: POST sync + DELETE; parse `SyncResponse`, decode `remoteData` when conflict.
- [ ] **Offline queue:** Queue sync when offline; retry when online; optional Core Data / SwiftData for persistence.
- [ ] Optional: Face ID / Touch ID for app unlock; certificate pinning for API.
- [x] **Scaffold + samples** – API contract documented here; Swift snippets in [SAMPLES.md](SAMPLES.md) (Keychain, offline queue outline, biometrics, pinning).
- [ ] Create Xcode project (Swift/SwiftUI); minimum deployment target iOS 15.0 (deliver in Xcode repo).
- [x] **Keychain pattern** – See SAMPLES.md `saveApiKey` / `loadApiKey`.
- [ ] Implement **PullAPI** in app (URLSession): GET endpoints above.
- [ ] Implement **SyncAPI** in app: POST sync + DELETE; parse `SyncResponse`.
- [x] **Offline queue** – Pattern described in SAMPLES.md; app-specific Core Data/SwiftData TBD in Xcode project.
- [x] **Optional auth/pinning** – Documented in SAMPLES.md; wire in app when needed.
## Discovery
@@ -25,5 +26,6 @@ This folder is a **scaffold** for the SMOA iOS app. The actual app is to be impl
## References
- [SAMPLES.md](SAMPLES.md) – Keychain, offline queue, Face ID/Touch ID, certificate pinning
- Backend: [backend/README.md](../../backend/README.md)
- Platform requirements: [docs/reference/PLATFORM-REQUIREMENTS.md](../reference/PLATFORM-REQUIREMENTS.md)
+47
View File
@@ -0,0 +1,47 @@
# iOS samples (Keychain, API key, offline queue)
Use with [docs/ios/README.md](../ios/README.md). These snippets are **reference only** (not a full Xcode project).
## Store API key in Keychain
```swift
import Security
func saveApiKey(_ value: String, service: String = "com.smoa.api") throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "apiKey",
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) }
}
func loadApiKey(service: String = "com.smoa.api") -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "apiKey",
kSecReturnData as String: true
]
var out: AnyObject?
guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess,
let data = out as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
```
## Simple offline sync queue (UserDefaults + retry)
Persist operation JSON strings; on `NWPathMonitor` satisfied, POST to `/api/v1/...` with `URLSession` and remove on success.
## Face ID / Touch ID
Wrap Keychain access with `LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, …)` before reading sensitive items.
## Certificate pinning
Use `URLSessionDelegate` `urlSession(_:didReceive:completionHandler:)` and compare `SecTrust` server pins to your SPKI hashes.
@@ -0,0 +1,36 @@
# Gaps and inconsistencies (cross-cutting audit)
**Last reviewed:** 2026-03-23
Most previously tracked gaps are **implemented**. This file lists only **long-horizon** or **compliance** items.
---
## Implemented (recent)
| Topic | Where |
|-------|--------|
| Shared OkHttp + Retrofit refresh | `SyncRetrofitHolder`, `NetworkModule`, `BackendSyncAPI` / `BackendPullAPI` lambdas |
| Non-blocking hosted config | `ClientConfigRefreshCoordinator.scheduleNonBlockingInitialLoad` |
| Browser VPN policy flag | `BuildConfig.SMOA_BROWSER_VPN_ENFORCED` / `-Psmoa.browser.vpnEnforced=true`, `VPNManager.setBrowserVpnEnforced` |
| Room credential cache | `credential_cache` + `CredentialCacheDatabaseModule` |
| OpenAPI drift process | `docs/development/OPENAPI-SYNCHRONIZATION.md`, `scripts/export-openapi-local.sh` |
---
## Remaining (long-term)
| Topic | Notes |
|-------|--------|
| **Strong multi-tenant isolation** | API key + `X-Unit` are not RLS; see `docs/security/TENANT-THREAT-MODEL.md`. |
| **AAMVA / ICAO production compliance** | Encoders need jurisdiction QA and official test vectors. |
| **Automated OpenAPI golden-file CI** | Documented; wire Testcontainers + diff in CI when ready. |
| **Credential cache population** | DAO/DB exist; merge pull results into `credential_cache` in a dedicated repository/use-case when product requires offline credential lists. |
---
## Related
- `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`
- `docs/schemas/`
- `backend/docs/BACKEND-GAPS-AND-ROADMAP.md`
@@ -0,0 +1,126 @@
# Identity templates: SMOA ↔ Complete Credential alignment
This document aligns **SMOA** credential templates (mobile presentation, barcode formats, sync payloads) with the **Complete Credential** umbrella program (`complete-credential` repo), in particular:
- Shared OpenAPI draft: `submodules/cc-shared-schemas/openapi/v1/openapi.yaml` — `CredentialRef.domain`
- Master plan credential domains (PIV, travel-adjacent, payment, NFC, mobile)
SMOA code lives in this repository; Complete Credential is an external reference layout. **Keep the string constants in sync** with:
- Android: `modules/credentials/.../SmoaCredentialTemplateIds.kt`
- Backend: `backend/.../SmoaCredentialType.kt` (same literal values)
---
## 1. Complete Credential `CredentialRef.domain`
From `cc-shared-schemas` (draft v0.1):
| `domain` value | Meaning (program) |
|----------------|-------------------|
| `payment` | Payment / financial instrument workflows |
| `piv_pki` | PIV-class, PKI-backed, high-assurance identity |
| `nfc_access` | Physical access, badge, NFC-adjacent credentials |
| `mobile` | Derived / mobile wallet–style credentials |
---
## 2. SMOA `credentialType` (canonical)
Use these values in **backend** `CredentialSyncRequest.credentialType`, **database** `credentials.credential_type`, and **mobile** when serializing sync payloads.
| `credentialType` | Maps to CC `domain` | SMOA implementation |
|------------------|---------------------|----------------------|
| `piv_pki` | `piv_pki` | PIV / smart card presentation; no single barcode class—use secure element / card APIs. Pair with org PKI metadata in `payload`. |
| `payment` | `payment` | Payment tokens / instruments (future wire-up to issuance services). |
| `nfc_access` | `nfc_access` | Access cards, mission badges; often QR/PDF417 from directory or access system. |
| `mobile` | `mobile` | Derived credentials, mobile-issued wallet artifacts. |
| `icao9303_mrtd` | *(travel-adjacent issuance — CC Phase / national programs)* | **ICAO Doc 9303** MRTD: `com.smoa.core.barcode.formats.ICAO9303Credential` |
| `aamva_dlid` | *(state ID — often under organizational / civil ID)* | **AAMVA** DL/ID PDF417: `com.smoa.core.barcode.formats.AAMVACredential` |
| `mil_std_129` | *(defense ID — organizational / sovereign)* | **MIL-STD-129**: `com.smoa.core.barcode.formats.MILSTD129Credential` |
| `agency_badge` | Often co-issued with `nfc_access` | Agency photo ID / badge; generic card UI when format is org-specific. |
### 2.1 Legacy aliases (older API / docs)
Prefer canonical types above for new integrations.
| Legacy | Prefer |
|--------|--------|
| `id` | `agency_badge` or `piv_pki` (context-dependent) |
| `badge` | `nfc_access` or `agency_badge` |
| `license` | `aamva_dlid` |
| `permit` | `agency_badge` or domain-specific extension in `payload` |
| `other` | Explicit new `credentialType` after design review |
---
## 3. Optional `payload` keys for cross-system identity
When syncing, `payload` (JSON object) may include:
| Key | Type | Description |
|-----|------|-------------|
| `completeCredentialDomain` | string | Echo CC `CredentialRef.domain`: `payment` \| `piv_pki` \| `nfc_access` \| `mobile` |
| `issuingAuthority` | object | **Preferred** for tenant / jurisdiction / ministry / agency (see `docs/schemas/credential-authority.schema.json`): `tenantId`, `jurisdiction`, `government`, `credentialRegistryRef`, etc. |
| `credentialId` | string | External issuance id (if different from sync `credentialId`) |
| `subjectId` | string (uuid) | Aligns with CC `SubjectRef.subjectId` when known |
Do **not** rely on a duplicate top-level `tenantId` alone; nest under `issuingAuthority` for consistency with JSON Schema.
Format-specific fields should match the Kotlin models (e.g. ICAO field names as JSON keys) when embedding structured data for rendering or barcode regeneration.
---
## 4. Barcode / format mapping (SMOA modules)
| `credentialType` | Kotlin model | Module / usage |
|------------------|--------------|----------------|
| `icao9303_mrtd` | `ICAO9303Credential` | `core/barcode`, `modules/credentials` |
| `aamva_dlid` | `AAMVACredential` | `core/barcode`, `modules/credentials` |
| `mil_std_129` | `MILSTD129Credential` | `core/barcode`, `modules/credentials` |
| `piv_pki` | *(no default barcode)* | PIV is typically X.509 / card APDU, not MRZ in-app |
| `nfc_access`, `agency_badge` | *(varies)* | Often backend-supplied bitmap or QR payload in `payload` |
---
## 5. Operational checklist
1. **Issuance systems** in Complete Credential should set `CredentialRef.domain` consistently; SMOA sync should set `credentialType` and optionally `completeCredentialDomain` to the same logical row in §2.
2. **Travel documents** use `icao9303_mrtd` and MRZ/check digit rules already implemented in `ICAO9303Credential`.
3. **PIV** uses `piv_pki`; do not conflate with `icao9303_mrtd` unless the credential is literally an eMRTD with ICAO structure.
4. When CC publishes expanded enums or template packs under `platform/entity-packs/`, update this file and the two Kotlin `SmoaCredential*` files in the same commit.
---
## 6. JSON Schema (issuance pipeline validation)
Payload shapes for **`icao9303_mrtd`**, **`aamva_dlid`**, and **`mil_std_129`** are defined under `docs/schemas/`:
| File | Purpose |
|------|---------|
| `credential-authority.schema.json` | Reusable **issuing authority** and **jurisdiction** (multi-government: tenant, country, subdivision, ministry, agency, service branch, localized names). |
| `credential-payload-icao9303_mrtd.schema.json` | MRZ-oriented travel document fields (YYMMDD). |
| `credential-payload-aamva_dlid.schema.json` | AAMVA-style DL/ID (YYYYMMDD). |
| `credential-payload-mil_std_129.schema.json` | Military ID-style card (YYYYMMDD). |
| `credential-payload-piv_pki.schema.json` | PIV/PKI optional payload. |
| `credential-payload-payment.schema.json` | Payment token refs. |
| `credential-payload-nfc_access.schema.json` | Access / badge metadata. |
| `credential-payload-mobile.schema.json` | Mobile / derived wallet refs. |
| `credential-payload-agency_badge.schema.json` | Agency photo ID / badge. |
| `credential-payload-document.schema.json` | **`oneOf`** union over all payload schemas (select by `credentialType` on the request body). |
Resolve **`$ref`** from the schema directory (same folder as the referring file). Schemas describe **`CredentialSyncRequest.payload` only** — the top-level **`credentialType`** field is on the sync JSON body alongside `credentialId`, `holderId`, etc.; do not duplicate `credentialType` inside `payload`. Use **`issuingAuthority`** in `payload` when syncing across ministries, agencies, or coalition programs.
The **`credential-payload-document.schema.json`** `oneOf` union is ambiguous without a discriminator on `payload`; issuance pipelines should select the schema file that matches **`request.credentialType`** after validating that string against `SmoaCredentialType.CANONICAL`.
---
## 7. Related paths
| Artifact | Path |
|----------|------|
| Client config example (FQDN / endpoints) | `backend/docs/examples/smoa-client-config.example.json` |
| LXC / hosting | `backend/docs/LXC-PROXMOX-CONTAINERS.md` |
| Backend credential entity | `backend/.../CredentialEntity.kt` |
| Sync DTO | `backend/.../CredentialSyncRequest` in `SyncRequest.kt` |
| OpenAPI (SMOA API) | `docs/api/api-specification.yaml` |
+16
View File
@@ -0,0 +1,16 @@
# Samsung Knox integration (optional)
SMOA targets Samsung devices (e.g. Galaxy Z Fold5) where **Knox** may be required for:
- **Knox VPN** or per-app VPN enforcement
- **Knox attestation** / device health for zero-trust policies
- **Sensitive data** in Knox container or TIMA-backed keystores
## Steps
1. Enroll in **Samsung Knox Partner Program** and obtain a license key.
2. Add Knox SDK dependencies (version aligned to **Knox API level** on your devices, e.g. 3.12).
3. Initialize the SDK in `Application.onCreate()` per Samsung docs.
4. Gate features with `KnoxUtils` / `EnterpriseDeviceManager` where policy requires.
This repository does **not** bundle the Knox SDK (distribution license). Use this guide when your deployment mandates Knox.
@@ -0,0 +1,129 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-authority.schema.json",
"title": "Issuing authority and jurisdiction (multi-government)",
"description": "Reusable context for credentials issued under different national governments, federated states, ministries, armed services, agencies, and subdivisions. Use inside credential sync payload JSON alongside format-specific fields.",
"$defs": {
"LocalizedString": {
"type": "object",
"required": ["language", "value"],
"properties": {
"language": {
"type": "string",
"pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$",
"description": "BCP 47 language tag (e.g. en, en-US, fr-CA)."
},
"value": {
"type": "string",
"description": "Human-readable text in that language."
}
},
"additionalProperties": false
},
"Jurisdiction": {
"type": "object",
"description": "Legal or administrative territory for the issuance act.",
"properties": {
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 3,
"pattern": "^([A-Z]{2}|[A-Z]{3})$",
"description": "ISO 3166-1 alpha-2 (preferred for APIs) or alpha-3."
},
"subdivisionCode": {
"type": "string",
"description": "ISO 3166-2 code (e.g. US-CA), national province/state code, or regional scheme."
},
"localityCode": {
"type": "string",
"description": "Municipality, county, district, indigenous territory, or other local identifier."
},
"supranationalBody": {
"type": "string",
"description": "When issuance is under EU, UN agency code, or other supranational framework."
}
},
"additionalProperties": true
},
"GovernmentBody": {
"type": "object",
"description": "Organizational hierarchy under a jurisdiction; not all fields apply to every country.",
"properties": {
"level": {
"type": "string",
"enum": [
"supranational",
"national",
"federal_union_member",
"state_provincial",
"regional",
"local",
"territorial",
"tribal_indigenous",
"international_organization",
"other"
],
"description": "Rough classification of the issuing government's level."
},
"branchOrPower": {
"type": "string",
"description": "Executive, legislative, judicial, or blended; label is jurisdiction-specific."
},
"ministryOrDepartment": {
"type": "string",
"description": "Cabinet ministry, federal department, or top-level civil organ."
},
"agencyCode": {
"type": "string",
"description": "Short stable code for the issuing agency within the tenant registry."
},
"agencyOfficialName": {
"type": "string",
"description": "Primary legal or registered name of the agency."
},
"serviceBranch": {
"type": "string",
"description": "Armed force, gendarmerie, coast guard, or national guard branch when applicable."
},
"organizationalUnit": {
"type": "string",
"description": "Subordinate command, directorate, regional office, station, or detachment."
},
"displayNames": {
"type": "array",
"description": "Localized names for the issuing body for presentation on device.",
"items": { "$ref": "#/$defs/LocalizedString" }
}
},
"additionalProperties": true
},
"IssuingAuthorityContext": {
"type": "object",
"description": "Who issued the credential in a multi-tenant, multi-state deployment.",
"properties": {
"tenantId": {
"type": "string",
"description": "Tenant / program partition (UUID, slug, or national registry id)."
},
"trustDomain": {
"type": "string",
"description": "Optional trust boundary (e.g. piv_pki, nfc_access) aligned with issuance architecture."
},
"jurisdiction": { "$ref": "#/$defs/Jurisdiction" },
"government": { "$ref": "#/$defs/GovernmentBody" },
"credentialRegistryRef": {
"type": "object",
"description": "Optional pointer to an external credential registry (e.g. Complete Credential).",
"properties": {
"credentialId": { "type": "string" },
"subjectId": { "type": "string", "format": "uuid" },
"registryUri": { "type": "string", "format": "uri" }
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
}
@@ -0,0 +1,87 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-aamva_dlid.schema.json",
"title": "Credential sync payload — AAMVA DL/ID",
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType on the sync request body). Use when request.credentialType is aamva_dlid. Aligns with com.smoa.core.barcode.formats.AAMVACredential.",
"type": "object",
"required": [
"documentDiscriminator",
"firstName",
"lastName",
"address",
"city",
"state",
"zipCode",
"dateOfBirth",
"expirationDate",
"issueDate",
"licenseNumber"
],
"properties": {
"completeCredentialDomain": {
"type": "string",
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
"description": "Optional mirror of Complete Credential CredentialRef.domain."
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"documentDiscriminator": {
"type": "string",
"description": "AAMVA document discriminator / instance id."
},
"firstName": { "type": "string" },
"middleName": { "type": "string" },
"lastName": { "type": "string" },
"address": { "type": "string" },
"city": { "type": "string" },
"state": {
"type": "string",
"description": "Subnational issuer code (e.g. US state, CA province) — national conventions apply."
},
"zipCode": {
"type": "string",
"description": "Postal code in issuing jurisdiction format."
},
"dateOfBirth": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"expirationDate": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"issueDate": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"licenseNumber": { "type": "string" },
"restrictions": { "type": "string" },
"endorsements": { "type": "string" },
"vehicleClass": { "type": "string" },
"height": {
"type": "string",
"description": "e.g. FTIN or metric per jurisdiction."
},
"weight": {
"type": "string",
"description": "e.g. LBS or KG per jurisdiction."
},
"eyeColor": { "type": "string" },
"hairColor": { "type": "string" },
"sex": {
"type": "string",
"enum": ["M", "F", "X"],
"description": "AAMVA allows X for unspecified/non-binary where adopted."
},
"extensions": {
"type": "object",
"description": "Jurisdiction-specific element codes, REAL ID flags, or non-AAMVA national DL profiles.",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-agency_badge.schema.json",
"title": "Credential sync payload — agency badge",
"description": "Organizational photo ID / badge when format is not ICAO/AAMVA/MIL-STD-129. Often paired with nfc_access issuance.",
"type": "object",
"properties": {
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"displayName": {
"type": "string"
},
"badgeNumber": {
"type": "string"
},
"photoRef": {
"type": "string",
"description": "URI or content-addressed ref to portrait; policy-dependent."
},
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-document.schema.json",
"title": "Credential sync payload — ICAO / AAMVA / MIL-STD-129 (discriminated union)",
"description": "Validates only the `payload` map inside CredentialSyncRequest — not the full request. `credentialType` lives on the request body; pick the matching branch (or validate against one schema after reading request.credentialType). oneOf may not auto-discriminate without extension keywords; prefer per-type validation in pipelines.",
"oneOf": [
{ "$ref": "credential-payload-icao9303_mrtd.schema.json" },
{ "$ref": "credential-payload-aamva_dlid.schema.json" },
{ "$ref": "credential-payload-mil_std_129.schema.json" },
{ "$ref": "credential-payload-piv_pki.schema.json" },
{ "$ref": "credential-payload-payment.schema.json" },
{ "$ref": "credential-payload-nfc_access.schema.json" },
{ "$ref": "credential-payload-mobile.schema.json" },
{ "$ref": "credential-payload-agency_badge.schema.json" }
]
}
@@ -0,0 +1,76 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-icao9303_mrtd.schema.json",
"title": "Credential sync payload — ICAO 9303 MRTD",
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType is on the sync request body, not duplicated here). Use when request.credentialType is icao9303_mrtd. Aligns with com.smoa.core.barcode.formats.ICAO9303Credential. Dates are MRZ-style YYMMDD.",
"type": "object",
"required": [
"documentType",
"issuingCountry",
"surname",
"givenNames",
"documentNumber",
"nationality",
"dateOfBirth",
"sex",
"expirationDate"
],
"properties": {
"completeCredentialDomain": {
"type": "string",
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
"description": "Optional mirror of Complete Credential CredentialRef.domain."
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"documentType": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "MRZ document type code (e.g. P passport, I ID card, A resident alien — per ICAO 9303)."
},
"issuingCountry": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "ISO 3166-1 alpha-3 issuing state."
},
"surname": { "type": "string" },
"givenNames": { "type": "string" },
"documentNumber": { "type": "string" },
"nationality": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "ISO 3166-1 alpha-3 nationality."
},
"dateOfBirth": {
"type": "string",
"pattern": "^\\d{6}$",
"description": "YYMMDD per ICAO MRZ."
},
"sex": {
"type": "string",
"enum": ["M", "F", "<"],
"description": "M, F, or < (unspecified) per ICAO."
},
"expirationDate": {
"type": "string",
"pattern": "^\\d{6}$",
"description": "YYMMDD document expiry."
},
"personalNumber": {
"type": "string",
"description": "Optional national personal number field when present on MRZ."
},
"optionalData": {
"type": "string",
"description": "Optional MRZ optional data field."
},
"extensions": {
"type": "object",
"description": "National variants, visible digital seal refs, or program-specific data; validate with national profiles if needed.",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,77 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-mil_std_129.schema.json",
"title": "Credential sync payload — MIL-STD-129 style military ID",
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType on the sync request body). Use when request.credentialType is mil_std_129. Aligns with com.smoa.core.barcode.formats.MILSTD129Credential.",
"type": "object",
"required": [
"serviceCode",
"lastName",
"firstName",
"socialSecurityNumber",
"dateOfBirth",
"expirationDate",
"issueDate",
"cardNumber"
],
"properties": {
"completeCredentialDomain": {
"type": "string",
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
"description": "Optional mirror of Complete Credential CredentialRef.domain."
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext",
"description": "Use government.serviceBranch, ministryOrDepartment (defense), and organizationalUnit (command) for multi-country defense organizations."
},
"serviceCode": {
"type": "string",
"description": "National service branch or component code (army, navy, air force, gendarmerie, etc.)."
},
"rank": { "type": "string" },
"lastName": { "type": "string" },
"firstName": { "type": "string" },
"middleInitial": {
"type": "string",
"maxLength": 4,
"description": "Single letter or short form used on ID."
},
"socialSecurityNumber": {
"type": "string",
"description": "National military ID number, service number, or last-4 style surrogate per policy; do not use full PII where prohibited."
},
"dateOfBirth": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"expirationDate": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"issueDate": {
"type": "string",
"pattern": "^\\d{8}$",
"description": "YYYYMMDD."
},
"cardNumber": {
"type": "string",
"description": "Document serial or PAN printed on the card."
},
"unit": {
"type": "string",
"description": "Unit, ship, squadron, station, or major command text."
},
"clearanceLevel": {
"type": "string",
"description": "National classification marking as shown or as tokenized for sync (policy-dependent)."
},
"extensions": {
"type": "object",
"description": "Coalition badge codes, NATO stock number refs, or country-specific pay grade / MOS fields.",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-mobile.schema.json",
"title": "Credential sync payload — mobile / derived",
"description": "Wallet-style or derived mobile credentials. Store wallet-specific references in extensions.",
"type": "object",
"properties": {
"completeCredentialDomain": {
"type": "string",
"const": "mobile"
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"walletCredentialRef": {
"type": "string",
"description": "Opaque reference from mobile issuance / MDM."
},
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-nfc_access.schema.json",
"title": "Credential sync payload — NFC / physical access",
"description": "Badge, PACS, or NFC access credential metadata. Payload often includes facility codes or QR bitmap refs in extensions.",
"type": "object",
"properties": {
"completeCredentialDomain": {
"type": "string",
"const": "nfc_access"
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"facilityCode": {
"type": "string"
},
"badgeNumber": {
"type": "string"
},
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-payment.schema.json",
"title": "Credential sync payload — payment",
"description": "Optional structured fields for credentialType payment (tokens, instruments). No card PANs in clear text; use token references and extensions per PCI/program policy.",
"type": "object",
"properties": {
"completeCredentialDomain": {
"type": "string",
"const": "payment"
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"paymentTokenRef": {
"type": "string",
"description": "Opaque token id from issuer switch / wallet."
},
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
@@ -0,0 +1,26 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smoa.dev/schemas/credential-payload-piv_pki.schema.json",
"title": "Credential sync payload — PIV / PKI",
"description": "Optional structured fields for credentialType piv_pki (FIPS 201 / smart card). Card APDU and cert details are often device-specific; use extensions for national profiles.",
"type": "object",
"properties": {
"completeCredentialDomain": {
"type": "string",
"const": "piv_pki",
"description": "Should mirror Complete Credential domain when used."
},
"issuingAuthority": {
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
},
"cardSerialHint": {
"type": "string",
"description": "Non-sensitive hint only; never full PAN/CHUID in clear if policy forbids."
},
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
+22
View File
@@ -0,0 +1,22 @@
# Tenant, API key, and unit header — threat model notes
## What exists today
- **API key** (`SMOA_API_KEY` / `X-API-Key`): shared-secret gate for `/api/v1/*` when configured. It does **not** identify a tenant or row-level security domain by itself.
- **`smoa.tenant.require-unit`**: when true, requests may require `X-Unit` (or equivalent) so clients must declare a unit; the backend can filter **read** paths that honor the header (see `TenantFilter`). This is **not** cryptographic proof of membership in that unit.
- **Multi-government payloads:** optional `issuingAuthority` in credential `payload_json` documents jurisdiction and org hierarchy for issuance audit; it does not enforce access control unless application logic is added.
## Gaps (explicit)
- **No** binding between API key and allowed `unit` / `holderId` / tenant id in the database layer.
- **No** row-level security (RLS) in PostgreSQL; all rows are visible to any authenticated client unless controllers add filters.
- **Compromise of API key** implies compromise of all data the backend stores until the key is rotated.
## Hardening directions
1. Issue **per-device or per-tenant** credentials (mTLS, JWT with `tenant_id` / `sub`, or OAuth2 client credentials) instead of a single static API key where feasible.
2. Map principal → **allowed units** in policy service; enforce in every sync/pull handler.
3. Enable **PostgreSQL RLS** or schema-per-tenant for strict isolation.
4. Log and monitor **`X-Request-Id`** and principal for sync audit (already partially covered by `sync_audit_log`).
See also `docs/reference/GAPS-AND-INCONSISTENCIES.md` and backend `TenantFilter`.
+1 -1
View File
@@ -197,7 +197,7 @@ For detailed compliance information, see:
## Remaining Work
**See [TODO.md](../../TODO.md)** for the full checklist of remaining and optional tasks (backend, Android, iOS, Web, infrastructure, compliance, testing).
**See [TODO.md](../../TODO.md)** (status vs external gates) and **[TASKS.md](../../TASKS.md)** (master task table with file links).
### Next steps (short-term)
+11
View File
@@ -0,0 +1,11 @@
# End-to-end test plan (future)
| Flow | Preconditions | Steps | Pass criteria |
|------|-----------------|-------|----------------|
| Sync directory | Backend up, API key set | Create entry via POST; pull GET; app offline queue flush | Data matches |
| Auth / RBAC | Test users seeded | Login restricted action | 403 without role |
| Meeting join | Stub or WebRTC test peer | Join → audio route | Connection state Connected |
**Tooling:** Maestro, Appium, or Espresso + MockWebServer for Android; XCUITest for iOS when the native app exists.
**CI:** Run backend `:backend:test` and Android unit tests on every push; reserve E2E for nightly or pre-release.
+35
View File
@@ -0,0 +1,35 @@
# Deploying the web scaffold
## Build
No bundler required. Serve the folder `docs/web-scaffold/` as static files over **HTTPS**.
## CORS
Set the backend for your web origin, e.g.:
```yaml
# application-prod.yml or env
smoa:
cors:
allowed-origins: "https://smoa.example.com"
```
## Nginx example
```nginx
location /web/ {
alias /var/www/smoa-web-scaffold/;
try_files $uri $uri/ /web/index.html;
add_header Cache-Control "no-cache" always;
}
```
## PWA
- `manifest.webmanifest` – install metadata (add PNG icons as needed).
- `sw.js` – registered from `index.html` for a minimal offline shell.
## CI
See [.gitea/workflows/ci.yml](../../.gitea/workflows/ci.yml) for backend tests; add a job to `rsync` this directory to your host if desired.
+6
View File
@@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#2563eb" />
<link rel="manifest" href="manifest.webmanifest" />
<title>SMOA Web</title>
<style>
* { box-sizing: border-box; }
@@ -33,6 +35,7 @@
<p id="dirStatus"></p>
</div>
<pre id="out"></pre>
<script src="offline-queue.js"></script>
<script>
function base() { return document.getElementById('baseUrl').value.replace(/\/$/, ''); }
function key() { return document.getElementById('apiKey').value.trim() || null; }
@@ -67,6 +70,9 @@
.catch(function(e) { dirStatus('Error: ' + e.message); dirList(''); })
.then(function() { btn.disabled = false; });
};
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(function () {});
}
</script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
{
"name": "SMOA Web",
"short_name": "SMOA",
"description": "SMOA API client and directory pull",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#2563eb",
"icons": []
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Minimal IndexedDB queue for sync operations (optional extension).
* Usage: queueSyncItem({ method, url, headers, body }); flushSyncQueue() when online.
*/
(function (global) {
const DB_NAME = 'smoa_sync_queue';
const STORE = 'pending';
function openDb() {
return new Promise(function (resolve, reject) {
const r = indexedDB.open(DB_NAME, 1);
r.onerror = function () { reject(r.error); };
r.onupgradeneeded = function () {
r.result.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
};
r.onsuccess = function () { resolve(r.result); };
});
}
global.queueSyncItem = function (item) {
return openDb().then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE, 'readwrite');
tx.objectStore(STORE).add({ created: Date.now(), item: item });
tx.oncomplete = function () { resolve(); };
tx.onerror = function () { reject(tx.error); };
});
});
};
global.flushSyncQueue = function (fetchImpl) {
var f = fetchImpl || fetch;
return openDb().then(function (db) {
return new Promise(function (resolve) {
const tx = db.transaction(STORE, 'readonly');
const req = tx.objectStore(STORE).getAll();
req.onsuccess = function () {
var rows = req.result || [];
resolve(rows);
};
});
}).then(function (rows) {
return Promise.all(
rows.map(function (row) {
var it = row.item;
return f(it.url, { method: it.method, headers: it.headers, body: it.body });
})
);
});
};
})(typeof window !== 'undefined' ? window : this);
+27
View File
@@ -0,0 +1,27 @@
/* SMOA web scaffold – minimal offline shell (cache static assets only). */
const CACHE = 'smoa-web-v1';
const ASSETS = ['./index.html', './manifest.webmanifest', './sw.js', './offline-queue.js'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.origin === self.location.origin && ASSETS.some((p) => url.pathname.endsWith(p.replace('./', '/')))) {
event.respondWith(caches.match(event.request).then((r) => r || fetch(event.request)));
return;
}
// API calls: network-first (no offline queue in this scaffold)
if (event.request.method === 'GET') {
event.respondWith(fetch(event.request).catch(() => caches.match('./index.html')));
}
});