> ## Documentation Index
> Fetch the complete documentation index at: https://x-preview-mintlify-7f49d7a1.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 문제 해결

> Chat XDK 오류, 안전한 키 백업 복구, 복호화 실패, 서명된 전송 페이로드 구축 등 흔한 X Chat 암호화 문제를 진단합니다.

이 페이지는 **X Chat 암호화와 Chat XDK에 특화된** 문제—키, 안전한 키 백업, 복호화/검증, 그리고 암호화된 전송 페이로드 구축—를 다룹니다.

웹훅, OAuth, HTTP 상태 코드 및 속도 제한은 일반 [X API](/x-api/introduction) 및 [인증](/fundamentals/authentication/overview) 문서를 사용하세요.

***

## 키 및 안전한 키 백업

### 잠금 해제 실패 (잘못된 패스코드)

* 패스코드가 `setup`에서 사용한 것과 일치하는지 확인하세요
* 시도 사이에 기다리세요; realm은 잘못된 추측에 대해 속도 제한을 두고 너무 많은 실패 후에는 복구를 잠글 수 있습니다

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    try:
        chat.unlock(passcode)
    except ValueError as e:
        print(e)  # may mention InvalidPin or guesses remaining
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    try {
      await chat.unlock(passcode);
    } catch (e) {
      console.error((e as Error).message);
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    chat.unlock(passcode_bytes).await?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    if err := chat.Unlock(passcode, juiceboxConfigJSON); err != nil {
        log.Println(err)
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    try { chat.Unlock(passcode, juiceboxConfigJson); }
    catch (Exception e) { Console.WriteLine(e.Message); }
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    try { chat.unlock(passcode, juiceboxConfigJson); }
    catch (Exception e) { System.out.println(e.getMessage()); }
    ```
  </Tab>
</Tabs>

### 키 또는 신원이 설정되지 않아 암호화 또는 복호화 실패

먼저 개인 키를 로드한 다음 **세션 신원**—당신의 사용자 ID와 X의 레코드에 있는 `public_key_version`—을 설정하세요. `encrypt_*` 및 `prepare_*` 메서드는 이를 사용해 서명합니다; 세션 신원 없이 (그리고 호출별 명시적 오버라이드도 없이) 이들을 호출하는 것은 오류입니다.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    chat.unlock(passcode)  # or: chat.import_keys(blob)
    chat.set_identity(my_user_id, signing_key_version)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    await chat.unlock(passcode);
    chat.setIdentity(myUserId, signingKeyVersion);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    chat.import_keys(&blob)?;
    chat.set_identity(&my_user_id, &signing_key_version);
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    blob, _ := chatxdk.Base64ToBytes(privateKeysB64)
    _ = chat.ImportKeys(blob)
    _ = chat.SetIdentity(myUserID, signingKeyVersion)
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    chat.ImportKeys(blobBytes);
    chat.SetIdentity(myUserId, signingKeyVersion);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    chat.importKeys(blobBytes);
    chat.setIdentity(myUserId, signingKeyVersion);
    ```
  </Tab>
</Tabs>

### 로컬 공개 키가 계정에 등록된 키와 절대 일치하지 않음

클라이언트는 종종 *"이 기기에 있는 키가 이 계정에 등록된 키 중 하나입니까?"* 라는 질문에 답해야 합니다 — 복원이나 임포트 후에 올바른 `public_key_version`을 채택하기 위해, 또는 온보딩이 이미 완료되었는지 결정하기 위해서입니다. Chat XDK의 `get_public_keys` 출력을 API의 `public_key` 필드와 **문자열로 비교하면 같은 키에 대해서도 항상 실패합니다**. 두 값이 서로 다른 인코딩을 사용하기 때문입니다:

* **API**는 등록에서 업로드한 대로 키를 저장하고 반환합니다: DER (SPKI) 인코딩 — 고정된 알고리즘 식별자 접두사 뒤의 원시 키
* **Chat XDK**의 `get_public_keys`는 그 접두사 없이 원시 키만 반환합니다

같은 키, 두 가지 표기입니다. 비교하려면 양쪽을 base64로 디코딩하고 API 바이트가 SDK 바이트로 **끝나는지** 확인하세요 (양쪽이 언젠가 같은 인코딩을 가지는 경우를 대비해 동일한 바이트도 일치로 간주합니다):

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import base64

    def same_key(local_b64: str, server_b64: str) -> bool:
        local = base64.b64decode(local_b64)    # chat.get_public_keys()["identity"]
        server = base64.b64decode(server_b64)  # API row's "public_key"
        return local == server or (len(server) > len(local) and server.endswith(local))
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const sameKey = (localB64: string, serverB64: string): boolean => {
      const local = Buffer.from(localB64, 'base64');   // chat.getPublicKeys().identity
      const server = Buffer.from(serverB64, 'base64'); // API row's public_key
      return local.equals(server) ||
        (server.length > local.length && server.subarray(server.length - local.length).equals(local));
    };
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    fn same_key(local: &[u8], server: &[u8]) -> bool {
        local == server || (server.len() > local.len() && server.ends_with(local))
    }
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func sameKey(local, server []byte) bool {
        return bytes.Equal(local, server) ||
            (len(server) > len(local) && bytes.HasSuffix(server, local))
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    static bool SameKey(byte[] local, byte[] server) =>
        local.SequenceEqual(server) ||
        (server.Length > local.Length &&
         server.AsSpan(server.Length - local.Length).SequenceEqual(local));
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    static boolean sameKey(byte[] local, byte[] server) {
        if (Arrays.equals(local, server)) return true;
        if (server.length <= local.length) return false;
        byte[] tail = Arrays.copyOfRange(server, server.length - local.length, server.length);
        return Arrays.equals(tail, local);
    }
    ```
  </Tab>
</Tabs>

일치하면 해당 행의 `public_key_version`을 `set_identity`에 채택하세요. 버전을 비교할 때(예: 최신 키를 선택하기 위해)는 **숫자로** 비교하세요 — 버전은 문자열 길이가 다양한 밀리초 타임스탬프이므로 사전식 비교는 잘못된 것을 선택합니다.

### 메시지에 대한 대화 키 누락

`Message encrypted with key version '…' but no matching key found`와 같은 오류는 해당 메시지의 `conversation_key_version`에 대한 **원시** 키가 없다는 뜻입니다.

1. `extract_conversation_keys`로 `conversation_key_change_event`(실시간 이벤트) 또는 `meta.conversation_key_events`(이력)의 키 자료를 복호화하거나, **또는** `decrypt_events`에 그 blob을 포함하세요—`set_cache_keys(true)`가 활성화된 상태에서는 `decrypt_events`가 각 대화의 최신 검증된 키도 유지하므로 이후의 `decrypt_event` 및 `encrypt_*` 호출에서는 이를 생략할 수 있습니다
2. 해당 버전에 대해 대화 키가 추가되었고 여전히 참가자인지 확인하세요 ([시작하기](/xchat/getting-started#4-set-up-conversation-keys) 참조)

### 상대방에게 공개 키가 없음

아직 온보딩을 마치지 않았을 수 있습니다. 그들이 등록한 후, **API 참조 → Encryption keys**에서 `public_key`, `signing_public_key`, `identity_public_key_signature`, `public_key_version`을 로드하세요.

***

## 복호화 및 서명

### 복호화 실패

* 오래되었거나 잘못된 **원시** 대화 키, 또는 잘못된 키 버전
* 불완전한 `encoded_event` 문자열
* 이벤트 유형이 복호화 가능한 콘텐츠로 처리할 수 있는 암호화된 메시지가 아님

### 서명 검증 실패

검증은 **기본적으로 실패-폐쇄**입니다(`reject_unverified = true`): SDK는 이미 검증되지 않은 서명 이벤트를 거부하므로, 여기서 실패가 발생한다면 검사를 켜야 한다는 뜻이 아니라 검증 입력이 잘못되었다는 뜻입니다. 일반적인 원인:

* **발신자**에 대한 서명 키 항목이 누락되었거나 불완전함 (Chat XDK가 요구하는 모든 필드—[Chat XDK](/xchat/xchat-xdk) 참조)
* 호출에서 서명 키가 전달되지 않았고 `set_signing_keys`로 저장된 것도 없음
* 발신자가 버전을 교체함—공개 키를 다시 가져오세요
* 허용된 최소값보다 낮은 키 버전은 절대 검증되지 않습니다
* **그룹 키 변경 이벤트**에서 서명자가 이미 그룹을 떠났다면 그들의 키가 더 이상 제공되지 않습니다 — [떠난 멤버로 인한 키 변경](/xchat/groups#떠난-멤버로-인한-키-변경) 참조

`set_reject_unverified` 세터는 이 기본값을 **비활성화**(`false`, 권장하지 않음)하기 위해 존재합니다. 이전에 비활성화했다면 실패-폐쇄 기본값으로 복원하세요:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    chat.set_reject_unverified(True)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    chat.setRejectUnverified(true);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    chat.set_reject_unverified(true);
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    chat.SetRejectUnverified(true)
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    chat.SetRejectUnverified(true);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    chat.setRejectUnverified(true);
    ```
  </Tab>
</Tabs>

### 답장에 `reply_preview_validation: "Invalid"`가 포함됨

복호화된 답장은 `reply_preview_validation`(`"Valid"` / `"Invalid"`; JavaScript는 `'valid'` / `'invalid'`)을 포함할 수 있습니다. `Invalid`는 메시지 내부의 인용된 미리보기가 임베드된 서명 원본 이벤트와 일치하지 않는다는 뜻입니다—인용을 신뢰할 수 없는 것으로 취급하고 인용 내용은 검증된 원본에서만 렌더링하세요. 메시지 자체는 별도로 검증되며 여전히 진본입니다; 미리보기가 유효하지 않다고 해서 예외가 발생하지는 않습니다.

### 오래된 이벤트가 영구적으로 검증에 실패함

**오래된** 이벤트에서 `signature missing or no matching signing key`나 ECDSA 불일치 같은 오류는 영구적입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증되므로, 다른 바이트로 서명된 (또는 서명되지 않은) 이벤트는 이후 로드 시마다 실패합니다—재시도, 키 갱신, 또는 API 호출로도 치유할 수 없습니다. 이러한 이벤트를 재시도 가능한 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점 이후부터 깨끗하고 검증 가능한 이력이 시작됩니다; 새 메시지는 영향을 받지 않습니다.

***

## 전송 페이로드 구축

이러한 실수들은 X Chat 암호화에 특화된 것입니다 (일반 HTTP 오류가 아님):

| 이슈             | 해결                                                                                                                                             |
| :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| 잘못된 키 바이트      | API의 암호화된 키 문자열이 아니라 **원시** 대화 키 바이트를 Chat XDK에 전달하세요                                                                                          |
| 잘못된 JSON 필드 이름 | `encrypted_content` → `encoded_message_create_event` 및 `encoded_event_signature` → `encoded_message_event_signature`로 매핑하세요                    |
| 잘못된 메시지 ID     | 반환된 페이로드의 `message_id`를 그대로 전송하세요—SDK가 이를 생성해 서명된 이벤트에 포함하므로, 다른 값을 쓰면 실패합니다. 재시도 시에는 동일한 암호화된 페이로드를 재사용하여 ID가 두 번 생성되지 않도록 하세요                |
| 버전 불일치         | `conversation_key_version`을 사용하는 키에 맞추고; `set_identity`에 전달하는 서명 키 버전을 공개 키 레코드와 일치시키세요                                                        |
| 경로 ID 형식       | URL 경로에는 여전히 하이픈으로 연결된 대화 ID가 필요합니다(`:` → `-`), 하지만 서명 시 SDK는 어떤 형태든 허용합니다: `A:B`, `A-B`(둘 중 어떤 순서든), 또는 단순한 수신자 사용자 ID—모두 동일한 서명된 바이트로 정규화됩니다 |

### 상태 변경 호출에서 API가 400을 반환함

모든 상태 변경 채팅 호출—대화 키 추가 또는 교체, 그룹 생성, 멤버 추가—에는 API 경계에서 검증되는 요청 본문의 \*\*`action_signatures`\*\*가 필요합니다. 누락되거나 잘못된 형식의 항목(각각 `message_id`, `encoded_message_event_detail`, 그리고 `signature`, `public_key_version`, `signature_version`이 있는 `message_event_signature`가 필요함)은 즉시 HTTP 400 problem-details 응답을 반환합니다. SDK prepare 메서드(`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`)를 사용하고 반환된 **모든** 서명을 보내세요—그룹 생성과 멤버 추가는 두 개를 반환합니다.

***

## 미디어 암호화 및 복호화

* 첨부 파일을 참조하는 메시지와 **동일한** 대화 키(및 버전)를 사용하세요
* `decrypt_stream`을 실행하기 전까지 다운로드 응답을 **암호문**으로 취급하세요
* MIME 유형은 복호화 **후**에 추론하세요; 다운로드 `Content-Type`은 종종 실제 이미지 유형이 아닙니다

세부 사항: [미디어](/xchat/media).

***

## 안전한 디버깅

암호화 실패를 조사할 때:

* 대화 ID, 이벤트 ID, 키 **버전**만 로그에 남기세요
* 평문, 패스코드, 개인 키, 또는 전체 키 blob을 로그에 **남기지 마세요**
* `set_identity`에 전달한 서명 키 버전이 공개 키 레코드의 `public_key_version`과 일치하는지 확인하세요
* 불완전한 이력의 경우, 복호화하기 전에 키 변경 메타데이터를 건너뛰지 않도록 **모든** 이벤트 페이지를 페이지 처리하세요
