Skip to content

Tuya SDK를 통해 살펴본 AWS Signed URL 기반 보안 이미지 접근 메커니즘 분석 #1

@pachuho

Description

@pachuho

클라우드 이미지 접근 시 보안 처리

카메라 기능 중 동작이 인식되면 그 순간을 포착해 이미지로 저장한다. 저장된 이미지는 S3 같은 클라우드에 저장되며 사용자가 원하는 시점에 이미지를 로딩하여 UI에 그린다. Tuya가 제공하는 SDK를 사용하며 손쉽게 구현할 수 있었지만 내부적으로 보안 처리가 어떻게 되어 있는지 제공된 문서가 없다. 가용한 데이터들을 통해 추정한 내용을 정리한다.

개요

"그러면 안될건데요?"

동료 서버 개발자와 클라우드에 저장된 이미지와 영상을 사용자에게 노출 시킬 때 어떤 구조로 데이터를 가져오는지 이야기를 나누었다.

  1. Tuya 서버를 거쳐 S3에 저장된 데이터를 가져온다.
  2. 직접 S3에 저장된 서버에서 데이터를 가져온다.

우리는 Tuya SDK를 사용하고 있으나 이에 대한 공식 문서나 제대로된 답변을 듣지 못해 로그를 확인해보았다.

https://xxx.s3.ap-northeast-2.amazonaws.com/xxx.jpeg?XXX@aaa

사용자가 접근할 수 있는 카메라 기기를 통해 이미지 정보를 가진 아이템 목록을 가져올 수 있다. 아이템 필드로는 s3 스토리지 저장소로 보이는 url 정보(@ 문자 이후로는 복호화에 사용되는 키라고 한다.)가 존재하며 이를 통해 ImageView에 이미지를 붙이거나 Bitmap으로 가져올 수 있다. 물론 웹상에 url만 가지곤 접근할 수 없도록 암호화된 형태다.

이를 근거로 서버 개발자에게 이미지나 영상은 SDK로부터 전달받은 URL과 복호화 키를 이용해 클라우드에서 직접 가져오는 것 같다고 이야기 했다. 하지만 서버 개발자는 해당 방식만으론 보안상 위험할 것 같다며 정확히 기억 나지 않지만 웹과 백엔드에선 유효기간과 암호화된 url을 통해 데이터에 접근한다며 의견을 냈다. 나는 SDK에서 이미지에 대한 url을 불러오기 위해선 유저 정보가 스마트폰에 캐싱된 상태이므로 내부적으로 복호화에 필요한 메타 데이터를 통해 처리 될 것 같다는 의견을 내며 이야기는 마무리 되었다.

분석

그럼 복호화에 필요한 메타 데이터는 무엇이고 어떻게 처리되는 것일까?
공식 문서나 내부 로직에 대한 답변을 받을 수 없었지만 라이브러리의 일부 코드가 난독화 되어 있지 않아 어느정도 추정을 할 수 있었다. 가장 먼저 Signed URL을 살펴보자.

https://xxx.s3.ap-northeast-2.amazonaws.com/exxx-tuya20602b52adf3cee5/detect/1744253619.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250411T144039Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=AKIA4MTWMRQDMDVZXXXX%2F20250410%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Signature=4a612c4bf9eb4ccd512d95630381a329d8fbf9f33b9d0ee9ca7354cd2394xxxx@3a0bb89cf7bexxxx

개인정보로 보이는 데이터들은 xxx로 축약 시켰다.

Tuya 공식문서에서 다음 함수로 이미지를 불러올 수 있다고 가이드 한다.

In an alert, the value of the URL for the attached image consists of the image URL and encryption key, which are concatenated in the format of {path}@{key}. To display the encrypted image, this concatenated string is parsed to get the decryption data.
If the string is not suffixed with @{key}, the attached image is not encrypted.

val img: SimpleDraweeView = findViewById(R.id.img);
if (mUriString.contains("@")) {
    val index = mUriString.lastIndexOf("@")
    try {
        val decryption = mUriString.substring(index + 1)
        val imageUrl = mUriString.substring(0, index)
        val builder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl))
                .setRotationOptions(RotationOptions.autoRotateAtRenderTime())
                .disableDiskCache()
        val imageRequest = DecryptImageRequest(builder, "AES", "AES/CBC/PKCS5Padding",
                decryption.toByteArray())
        controller = Fresco.newDraweeControllerBuilder().setImageRequest(imageRequest)
                .build()
    } catch (e: Exception) {
        e.printStackTrace()
    }
} else {
    try {
        uri = Uri.parse(mUriString)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    val builder = Fresco.newDraweeControllerBuilder().setUri(uri)
    controller = builder.build()
}
img.controller = controller

코드를 간단히 살펴보면 @ 문자를 기준으로 전자는 이미지 URL, 후자는 복호화 키로 사용하며
SDK에서 제공하는 함수 setImageURI에 값을 넣으면 이미지가 attach 된다.
AES 암호화 알고리즘을 통해 복호화를 시도하며 처리된 이미지는 Facebook에서 개발한 fresco 라이브러리를 통해 이미지 로딩 이루어 지는 것으로 보인다. 복호화 과정을 알기 위해 DecryptImageRequest 클래스를 살펴보자

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions