obscura-server 0.3.2

A server for relaying secure messages using the Signal Protocol
Documentation
openapi: 3.0.3
info:
  title: Obscura Server API
  description: |
    A privacy-first messaging relay.
    - **Control Plane (REST):** Registration, Key Management, Sending.
    - **Data Plane (WebSocket):** Real-time message delivery and acknowledgement.
    - **Protocol:** Protocol Buffers (application/x-protobuf) are used for message bodies to minimize metadata leakage and binary bloat.
  version: 0.0.0
paths:
  # --- Account & Identity ---
  /v1/users:
    post:
      summary: Register a new user device.
      tags: [Account]
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegistrationRequest'
      responses:
        '201':
          description: Account created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '400':
          description: Invalid input or username already exists.

  /v1/sessions:
    post:
      summary: Login and retrieve a session token.
      tags: [Account]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
                password:
                  type: string
              required:
                - username
                - password
      responses:
        '200':
          description: Authenticated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          description: Unauthorized (Invalid credentials).

    delete:
      summary: Logout (Revoke Refresh Token).
      description: |
        Revokes the provided Refresh Token, effectively logging the user out of that session.
      tags: [Account]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        '200':
          description: Session revoked.

  /v1/sessions/refresh:
    post:
      summary: Refresh Access Token.
      description: |
        Exchanges a valid Refresh Token for a new pair (Access Token + Refresh Token).
        Implements Rotation: The old Refresh Token is invalidated immediately.
      tags: [Account]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        '200':
          description: Tokens refreshed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          description: Unauthorized (Invalid or expired refresh token).

  # --- Keys (X3DH) ---
  /v1/keys:
    post:
      summary: Upload PreKeys or Perform Device Takeover.
      description: |
        Uploads new Signed PreKeys and One-Time PreKeys.

        **Device Takeover:**
        If an `identityKey` is provided and it *differs* from the stored key for this user:
        - Replaces the Identity Key.
        - Deletes ALL old keys (Signed and One-Time).
        - Deletes ALL pending messages.
        - Disconnects active WebSockets.

        If `identityKey` matches the stored key or is omitted, it acts as a standard key refill (appending new keys).
      tags: [Keys]
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreKeyUpload'
      responses:
        '200':
          description: Keys updated or takeover successful.
        '400':
          description: Invalid key formats, invalid signature, pre-key limit exceeded, or missing registrationId for takeover.
        '401':
          description: Unauthorized.

  /v1/keys/{userId}:
    get:
      summary: Fetch PreKey Bundle.
      description: |
        Returns active PreKey bundle for the target user.
        Server atomically consumes one One-Time PreKey (if available).
      tags: [Keys]
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: PreKey Bundle.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreKeyBundle'
        '404':
          description: User not found or no keys available.

  # --- Messaging (REST Upstream) ---
  /v1/messages/{recipientId}:
    post:
      summary: Send an encrypted message (Push).
      description: |
        Pushes an encrypted envelope to the target device's queue.
        **Payload:** `OutgoingMessage` (Protobuf).
        The recipient will receive this via WebSocket.
      tags: [Messaging]
      security:
        - bearerAuth: []
      parameters:
        - name: recipientId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/x-protobuf:
            schema:
              type: string
              format: binary
              description: Serialized `OutgoingMessage` protobuf.
          application/octet-stream:
             schema:
              type: string
              format: binary
              description: Serialized `OutgoingMessage` protobuf.
      responses:
        '201':
          description: Message queued.
        '400':
          description: Invalid Protobuf.
        '401':
          description: Unauthorized.
        '404':
          description: Recipient not found.

  # --- WebSocket Gateway (Documentation Only) ---
  /v1/gateway:
    get:
      summary: Connect to the Message Stream.
      description: |
        **WebSocket Endpoint.**
        Clients must connect here to receive messages.
        - **Protocol:** `WebSocketFrame` (Protobuf).
        - **Auth:** Pass the JWT in the query string: `ws://.../v1/gateway?token=<jwt>`.
        - **Flow:** Server pushes `IncomingEnvelope`. Client responds with `AckMessage`. Server deletes message.
      tags: [Messaging]
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: JWT Access Token.
      responses:
        '101':
          description: Switching Protocols.
        '401':
          description: Unauthorized (Invalid or missing token).

  # --- Attachments (Binary Storage) ---
  /v1/attachments:
    post:
      summary: Upload an attachment.
      description: |
        Uploads an encrypted binary blob to long-term storage (S3).
        Blob will be auto-deleted after the configured TTL.
      tags: [Attachments]
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '201':
          description: Upload successful.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  expiresAt:
                    type: integer
                    format: int64
                    description: UNIX timestamp of when the file will be deleted.
        '400':
          description: File too large.
        '401':
          description: Unauthorized.

  /v1/attachments/{id}:
    get:
      summary: Download an attachment.
      tags: [Attachments]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Binary file stream.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          description: Unauthorized.
        '404':
          description: Not found or expired.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    AuthResponse:
      type: object
      required:
        - token
        - refreshToken
        - expiresAt
      properties:
        token:
          type: string
          description: Short-lived JWT Access Token.
        refreshToken:
          type: string
          description: Long-lived opaque Refresh Token.
        expiresAt:
          type: integer
          format: int64
          description: UNIX timestamp of access token expiration.

    RegistrationRequest:
      type: object
      required:
        - username
        - password
        - identityKey
        - registrationId
        - signedPreKey
        - oneTimePreKeys
      properties:
        username:
          type: string
        password:
          type: string
        identityKey:
          type: string
          description: Base64 encoded public identity key.
        registrationId:
          type: integer
        signedPreKey:
          $ref: '#/components/schemas/SignedPreKeyDto'
        oneTimePreKeys:
          type: array
          items:
            $ref: '#/components/schemas/OneTimePreKeyDto'

    PreKeyUpload:
      type: object
      required:
        - signedPreKey
        - oneTimePreKeys
      properties:
        identityKey:
          type: string
          description: Optional Base64 Identity Key. Triggers takeover if changed.
        registrationId:
          type: integer
          description: Required if identityKey is provided.
        signedPreKey:
          $ref: '#/components/schemas/SignedPreKeyDto'
        oneTimePreKeys:
          type: array
          items:
            $ref: '#/components/schemas/OneTimePreKeyDto'

    PreKeyBundle:
      type: object
      properties:
        registrationId:
          type: integer
        identityKey:
          type: string
          format: byte
        signedPreKey:
          $ref: '#/components/schemas/SignedPreKeyDto'
        oneTimePreKey:
          $ref: '#/components/schemas/OneTimePreKeyDto'
          nullable: true

    SignedPreKeyDto:
      type: object
      required:
        - keyId
        - publicKey
        - signature
      properties:
        keyId:
          type: integer
        publicKey:
          type: string
          description: Base64 encoded public key
        signature:
          type: string
          description: Base64 encoded signature

    OneTimePreKeyDto:
      type: object
      required:
        - keyId
        - publicKey
      properties:
        keyId:
          type: integer
        publicKey:
          type: string
          description: Base64 encoded public key