저는 Supabase Storage에 저장된 암호화 파일을 직접 내려받으려다가, 브라우저가 알아보기 힘든 바이너리만 던져준다는 사실을 깨달았습니다. 그래서 서버에서 복호화한 뒤 사용자에게 적절한 헤더와 함께 전달하는 방식을 만들었습니다.
application/octet-stream으로 처리했습니다.filename* 포맷으로 인코딩해 브라우저가 깨지지 않도록 했습니다.암호화된 키와 IV를 조회한 뒤, KMS에서 키를 복호화하고 AES-GCM으로 원본 데이터를 얻습니다.
import { decryptDataKey } from './kms';
import { aesGcmDecrypt, validateAndConvertKey, base64ToArrayBuffer } from './crypto';
import { storageClient, getKeyRecord, parseFileName } from './storage';
export const downloadEncryptedFile = async (path: string, bucket: string) => {
const { encrypted_key, iv } = await getKeyRecord(path);
const encryptedFile = await storageClient.from(bucket).download(path);
const plaintextKey = await decryptDataKey('file', encrypted_key);
const decrypted = await aesGcmDecrypt(
encryptedFile.data,
validateAndConvertKey(plaintextKey),
base64ToArrayBuffer(iv),
);
return { decryptedData: decrypted, fileName: parseFileName(path) };
};
API 라우트에서는 ArrayBuffer를 Blob으로 감싼 후 new Response로 반환합니다. 이미지면 인라인으로 보여주고, 그 외에는 Content-Disposition: attachment를 설정합니다.
POST는 JSON body로, GET은 쿼리스트링으로 경로와 버킷을 받습니다. 이름을 지정하면 name 파라미터로 덮어씁니다.
image/로 시작하면 Content-Disposition을 생략했습니다.bucket 파라미터를 필수로 받도록 했습니다.지금은 사용자가 암호화된 파일을 자연스럽게 내려받을 수 있고, 이미지 미리보기까지 지원합니다. 스토리지와 키가 분리되어 있어 안전하면서도 UX를 해치지 않게 된 셈이죠. 다음에는 다운로드 로그를 남겨 누가 언제 어떤 파일을 열람했는지 추적해볼 계획입니다.
여러분은 암호화 파일을 어떻게 전달하고 계신가요? 더 나은 방법이 있다면 댓글로 공유해주세요.