[2026] DRM 완벽 가이드 | Widevine·FairPlay·PlayReady·AES-128 총정리

[2026] DRM 완벽 가이드 | Widevine·FairPlay·PlayReady·AES-128 총정리

이 글의 핵심

DRM(Digital Rights Management) 기술의 모든 것. Widevine, FairPlay, PlayReady, AES-128 암호화, EME, CENC 표준부터 Netflix, YouTube, Spotify의 DRM 구현까지 실전 예제로 완벽 이해.

들어가며: DRM이 필요한 이유

Netflix, Disney+, Spotify 같은 스트리밍 서비스는 어떻게 콘텐츠를 보호할까요? 답은 DRM(Digital Rights Management)입니다. DRM은 디지털 콘텐츠의 불법 복제를 방지하고, 승인된 사용자만 재생할 수 있게 하는 기술입니다. 이 글에서 다룰 내용:

  • 주요 DRM 시스템 (Widevine, FairPlay, PlayReady)
  • DRM 동작 원리와 아키텍처
  • EME (Encrypted Media Extensions)
  • CENC (Common Encryption) 표준
  • 실전 구현 예제

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

목차

  1. DRM 기본 개념
  2. 주요 DRM 시스템
  3. Widevine (Google)
  4. FairPlay (Apple)
  5. PlayReady (Microsoft)
  6. EME와 CENC
  7. HLS AES-128 암호화
  8. 실전 구현
  9. DRM 우회 방지
  10. 비교 및 선택 가이드

1. DRM 기본 개념

DRM이란?

DRM(Digital Rights Management)은 디지털 콘텐츠의 암호화, 라이선스 관리, 접근 제어를 통해 저작권을 보호하는 기술입니다.

DRM 구성 요소

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Content[콘텐츠 제공자]
        Video[원본 비디오]
        Encoder[인코더/패키저]
        KeyServer[키 서버]
    end
    
    subgraph CDN[CDN]
        Encrypted["암호화된\n콘텐츠"]
    end
    
    subgraph Client[클라이언트]
        Player[비디오 플레이어]
        CDM["CDM\nContent Decryption Module"]
        TEE["TEE/Hardware\n보안 영역"]
    end
    
    subgraph License[라이선스 서버]
        Auth[인증]
        LicenseDB[라이선스 DB]
    end
    
    Video --> Encoder
    Encoder -->|암호화| Encrypted
    Encoder -->|키 저장| KeyServer
    
    Encrypted --> Player
    Player -->|라이선스 요청| Auth
    Auth -->|검증| LicenseDB
    LicenseDB -->|키 전달| CDM
    CDM -->|복호화| TEE
    TEE -->|재생| Player

DRM 동작 흐름

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant User as 사용자
    participant Player as 플레이어
    participant CDN
    participant License as 라이선스 서버
    participant CDM as CDM (보안 모듈)
    
    User->>Player: 비디오 재생 요청
    Player->>CDN: 암호화된 콘텐츠 요청
    CDN->>Player: 암호화된 세그먼트 전달
    
    Player->>License: 라이선스 요청\n(사용자 인증 토큰)
    License->>License: 권한 확인\n(구독, 지역, 기기)
    License->>Player: 라이선스 + 복호화 키
    
    Player->>CDM: 복호화 요청
    CDM->>CDM: 하드웨어 보안 영역에서\n복호화
    CDM->>Player: 복호화된 프레임
    Player->>User: 비디오 재생

2. 주요 DRM 시스템

DRM 생태계

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Google
        Widevine[Widevine]
        Chrome[Chrome]
        Android[Android]
    end
    
    subgraph Apple
        FairPlay[FairPlay]
        Safari[Safari]
        iOS[iOS/tvOS]
    end
    
    subgraph Microsoft
        PlayReady[PlayReady]
        Edge[Edge]
        Xbox[Xbox]
    end
    
    subgraph Open
        ClearKey[ClearKey]
        AES128[HLS AES-128]
    end
    
    Widevine --> Chrome
    Widevine --> Android
    
    FairPlay --> Safari
    FairPlay --> iOS
    
    PlayReady --> Edge
    PlayReady --> Xbox

플랫폼별 DRM 지원

플랫폼WidevineFairPlayPlayReadyClearKey
Chrome✅ L1/L3
Safari
Edge
Firefox✅ L3
Android✅ L1/L3
iOS
Xbox

3. Widevine (Google)

Widevine이란?

Widevine은 Google이 개발한 DRM으로, Android와 Chrome에서 널리 사용됩니다. Netflix, YouTube Premium, Disney+가 사용합니다.

Widevine 보안 레벨

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph L1["Widevine L1\nHardware-backed"]
        L1_Desc["✅ TEE/Secure Processor\n✅ 4K/HDR 지원\n✅ 최고 보안"]
    end
    
    subgraph L2["Widevine L2\nSoftware-backed"]
        L2_Desc["⚠️ 소프트웨어 보안\n⚠️ 1080p 제한\n⚠️ 중간 보안"]
    end
    
    subgraph L3["Widevine L3\nSoftware only"]
        L3_Desc["❌ 기본 보안\n❌ 480p/SD 제한\n❌ 낮은 보안"]
    end
    
    L1 --> Best[최고 화질]
    L2 --> Medium[중간 화질]
    L3 --> Low[낮은 화질]

Widevine 아키�ecture

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph App[애플리케이션]
        Player[Video Player]
        EME[EME API]
    end
    
    subgraph Browser[브라우저]
        CDM[Widevine CDM]
    end
    
    subgraph Hardware["하드웨어 (L1)"]
        TEE["Trusted Execution\nEnvironment"]
        SecureVideo[Secure Video Path]
    end
    
    subgraph Backend[백엔드]
        License["Widevine\nLicense Server"]
        Content["암호화된\n콘텐츠"]
    end
    
    Player --> EME
    EME --> CDM
    CDM --> TEE
    
    Player --> Content
    CDM --> License
    
    TEE --> SecureVideo
    SecureVideo --> Display[디스플레이]

Widevine 구현 예시

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// HTML5 Video + EME (Encrypted Media Extensions)
const video = document.querySelector('video');
const config = [{
  initDataTypes: ['cenc'],
  videoCapabilities: [{
    contentType: 'video/mp4; codecs="avc1.42E01E"',
    robustness: 'SW_SECURE_CRYPTO'  // L3
    // robustness: 'HW_SECURE_ALL'   // L1
  }],
  audioCapabilities: [{
    contentType: 'audio/mp4; codecs="mp4a.40.2"',
    robustness: 'SW_SECURE_CRYPTO'
  }]
}];
// MediaKeys 생성
navigator.requestMediaKeySystemAccess('com.widevine.alpha', config)
  .then(keySystemAccess => {
    return keySystemAccess.createMediaKeys();
  })
  .then(mediaKeys => {
    return video.setMediaKeys(mediaKeys);
  })
  .then(() => {
    console.log('✅ Widevine initialized');
    
    // 비디오 로드
    video.src = 'https://cdn.example.com/encrypted-video.mp4';
  })
  .catch(err => {
    console.error('❌ DRM error:', err);
  });
// 라이선스 요청 처리
video.addEventListener('encrypted', (event) => {
  const session = video.mediaKeys.createSession();
  
  session.addEventListener('message', (event) => {
    // 라이선스 서버에 요청
    fetch('https://license.example.com/widevine', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/octet-stream',
        'Authorization': 'Bearer ' + userToken
      },
      body: event.message
    })
    .then(response => response.arrayBuffer())
    .then(license => {
      // 라이선스 적용
      return session.update(license);
    })
    .then(() => {
      console.log('✅ License acquired');
    })
    .catch(err => {
      console.error('❌ License error:', err);
    });
  });
  
  // 세션 생성
  session.generateRequest(event.initDataType, event.initData);
});

Widevine 레벨 확인

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 브라우저에서 Widevine 레벨 확인
async function checkWidevineLevel() {
  const configs = [
    {
      label: 'L1 (Hardware)',
      config: [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"',
          robustness: 'HW_SECURE_ALL'
        }]
      }]
    },
    {
      label: 'L3 (Software)',
      config: [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"',
          robustness: 'SW_SECURE_CRYPTO'
        }]
      }]
    }
  ];
  
  for (const {label, config} of configs) {
    try {
      await navigator.requestMediaKeySystemAccess('com.widevine.alpha', config);
      console.log(`✅ ${label} supported`);
      return label;
    } catch (e) {
      console.log(`❌ ${label} not supported`);
    }
  }
  
  return 'Not supported';
}
checkWidevineLevel();

4. FairPlay (Apple)

FairPlay란?

FairPlay는 Apple이 개발한 DRM으로, iOS, macOS, tvOS, Safari에서 사용됩니다. HLS (HTTP Live Streaming)와 통합되어 있습니다.

FairPlay 동작 흐름

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant Player as AVPlayer
    participant App as 앱
    participant KSM as Key Server Module
    participant License as FairPlay License Server
    
    Player->>App: 암호화된 콘텐츠 감지
    App->>KSM: SPC 요청\n(Server Playback Context)
    KSM->>App: SPC 생성
    
    App->>License: SPC + 인증 토큰
    License->>License: 권한 검증
    License->>App: CKC\n(Content Key Context)
    
    App->>KSM: CKC 전달
    KSM->>Player: 복호화 키
    Player->>Player: 콘텐츠 재생

FairPlay 구현 (iOS/Swift)

다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import AVFoundation
class FairPlayManager: NSObject {
    var asset: AVURLAsset?
    var resourceLoaderDelegate: FairPlayResourceLoaderDelegate?
    
    func playVideo(url: URL, certificateURL: URL, licenseURL: URL) {
        // AVURLAsset 생성
        asset = AVURLAsset(url: url)
        
        // Resource Loader Delegate 설정
        resourceLoaderDelegate = FairPlayResourceLoaderDelegate(
            certificateURL: certificateURL,
            licenseURL: licenseURL
        )
        
        asset?.resourceLoader.setDelegate(
            resourceLoaderDelegate,
            queue: DispatchQueue.main
        )
        
        // AVPlayer로 재생
        let playerItem = AVPlayerItem(asset: asset!)
        let player = AVPlayer(playerItem: playerItem)
        player.play()
    }
}
class FairPlayResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate {
    let certificateURL: URL
    let licenseURL: URL
    var certificate: Data?
    
    init(certificateURL: URL, licenseURL: URL) {
        self.certificateURL = certificateURL
        self.licenseURL = licenseURL
        super.init()
        
        // 인증서 다운로드
        loadCertificate()
    }
    
    func loadCertificate() {
        URLSession.shared.dataTask(with: certificateURL) { data, _, error in
            if let data = data {
                self.certificate = data
                print("✅ FairPlay certificate loaded")
            }
        }.resume()
    }
    
    func resourceLoader(
        _ resourceLoader: AVAssetResourceLoader,
        shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
    ) -> Bool {
        guard let url = loadingRequest.request.url,
              let certificate = certificate else {
            return false
        }
        
        // skd:// URL 처리
        guard url.scheme == "skd" else {
            return false
        }
        
        // SPC (Server Playback Context) 생성
        let contentId = url.host ?? ""
        let contentIdData = contentId.data(using: .utf8)!
        
        do {
            let spcData = try loadingRequest.streamingContentKeyRequestData(
                forApp: certificate,
                contentIdentifier: contentIdData,
                options: nil
            )
            
            // 라이선스 서버에 요청
            requestLicense(spc: spcData, contentId: contentId) { ckcData in
                if let ckc = ckcData {
                    // CKC (Content Key Context) 적용
                    loadingRequest.dataRequest?.respond(with: ckc)
                    loadingRequest.finishLoading()
                    print("✅ FairPlay license acquired")
                } else {
                    loadingRequest.finishLoading(with: NSError(
                        domain: "FairPlay",
                        code: -1,
                        userInfo: nil
                    ))
                }
            }
            
            return true
            
        } catch {
            print("❌ SPC generation failed: \(error)")
            return false
        }
    }
    
    func requestLicense(spc: Data, contentId: String, completion: @escaping (Data?) -> Void) {
        var request = URLRequest(url: licenseURL)
        request.httpMethod = "POST"
        request.httpBody = spc
        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                completion(data)
            } else {
                completion(nil)
            }
        }.resume()
    }
}

FairPlay HLS 매니페스트

다음은 m3u8를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
# FairPlay 키 정보
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://content-id-12345",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"
# 세그먼트
#EXTINF:10.0,
segment-0.m4s
#EXTINF:10.0,
segment-1.m4s
#EXTINF:10.0,
segment-2.m4s

5. PlayReady (Microsoft)

PlayReady란?

PlayReady는 Microsoft가 개발한 DRM으로, Windows, Xbox, Edge에서 사용됩니다.

PlayReady 아키텍처

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Client[클라이언트]
        App[애플리케이션]
        PRT[PlayReady Runtime]
        TEE["Trusted Execution\nEnvironment"]
    end
    
    subgraph Server[서버]
        Content[암호화된 콘텐츠]
        License["PlayReady\nLicense Server"]
    end
    
    App --> PRT
    PRT --> TEE
    
    App --> Content
    PRT --> License
    
    License -->|라이선스| PRT
    Content -->|암호화된 데이터| PRT
    PRT -->|복호화| TEE

PlayReady 구현 (C#)

다음은 csharp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

using Windows.Media.Protection;
using Windows.Media.Protection.PlayReady;
public class PlayReadyManager
{
    private MediaProtectionManager protectionManager;
    
    public void Initialize()
    {
        // MediaProtectionManager 생성
        protectionManager = new MediaProtectionManager();
        
        // PlayReady 서비스 요청 처리
        protectionManager.ServiceRequested += OnServiceRequested;
        
        // 컴포넌트 로드 실패 처리
        protectionManager.ComponentLoadFailed += OnComponentLoadFailed;
        
        // PlayReady 속성 설정
        var props = new Windows.Foundation.Collections.PropertySet();
        props.Add(
            "{F4637010-03C3-42CD-B932-B48ADF3A6A54}",
            "Windows.Media.Protection.PlayReady.PlayReadyWinRTTrustedInput"
        );
        protectionManager.Properties.Add("Windows.Media.Protection.MediaProtectionSystemId", 
            "{F4637010-03C3-42CD-B932-B48ADF3A6A54}");
        protectionManager.Properties.Add("Windows.Media.Protection.MediaProtectionSystemIdMapping", 
            props);
    }
    
    private async void OnServiceRequested(
        MediaProtectionManager sender,
        ServiceRequestedEventArgs args)
    {
        if (args.Request is PlayReadyIndividualizationServiceRequest)
        {
            // 개별화 요청
            var request = args.Request as PlayReadyIndividualizationServiceRequest;
            await request.BeginServiceRequest();
        }
        else if (args.Request is PlayReadyLicenseAcquisitionServiceRequest)
        {
            // 라이선스 요청
            var request = args.Request as PlayReadyLicenseAcquisitionServiceRequest;
            
            // 커스텀 헤더 추가 (인증 토큰 등)
            request.ChallengeCustomData = GetAuthToken();
            
            await request.BeginServiceRequest();
            Console.WriteLine("✅ PlayReady license acquired");
        }
    }
    
    private void OnComponentLoadFailed(
        MediaProtectionManager sender,
        ComponentLoadFailedEventArgs args)
    {
        Console.WriteLine($"❌ Component load failed: {args.Information.Items}");
    }
    
    private string GetAuthToken()
    {
        // 사용자 인증 토큰 생성
        return "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
    }
}

6. EME와 CENC

EME (Encrypted Media Extensions)

EME는 W3C 표준으로, 브라우저에서 DRM 콘텐츠를 재생하기 위한 JavaScript API입니다. 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// EME 지원 확인
if ('mediaKeys' in HTMLMediaElement.prototype) {
  console.log('✅ EME supported');
} else {
  console.log('❌ EME not supported');
}
// 지원되는 DRM 시스템 확인
const drmSystems = [
  'com.widevine.alpha',           // Widevine
  'com.apple.fps.1_0',            // FairPlay
  'com.microsoft.playready',      // PlayReady
  'org.w3.clearkey'               // ClearKey
];
for (const system of drmSystems) {
  try {
    await navigator.requestMediaKeySystemAccess(system, [{}]);
    console.log(`✅ ${system} supported`);
  } catch (e) {
    console.log(`❌ ${system} not supported`);
  }
}

CENC (Common Encryption)

CENC는 하나의 암호화된 파일을 여러 DRM 시스템에서 사용할 수 있게 하는 표준입니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    Original[원본 비디오] --> Encoder[인코더]
    
    Encoder --> CENC["CENC 암호화\n단일 파일"]
    
    CENC --> Chrome["Chrome\nWidevine"]
    CENC --> Safari["Safari\nFairPlay"]
    CENC --> Edge["Edge\nPlayReady"]
    
    Chrome --> Play1[재생]
    Safari --> Play2[재생]
    Edge --> Play3[재생]

CENC 암호화 (Shaka Packager)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# Shaka Packager로 CENC 암호화
packager \
  in=input.mp4,stream=video,output=video_encrypted.mp4 \
  in=input.mp4,stream=audio,output=audio_encrypted.mp4 \
  --enable_raw_key_encryption \
  --keys label=:key_id=<KEY_ID>:key=<KEY> \
  --protection_scheme cenc \
  --clear_lead 3 \
  --mpd_output manifest.mpd
# 여러 DRM 시스템 지원
packager \
  in=input.mp4,stream=video,output=video.mp4 \
  --enable_widevine_encryption \
  --key_server_url "https://license.widevine.com/cenc/getcontentkey" \
  --content_id "content-123" \
  --signer "widevine_test" \
  --enable_playready_encryption \
  --playready_server_url "https://playready.example.com" \
  --enable_fairplay_encryption \
  --fairplay_key_uri "skd://fairplay-key-123"

7. HLS AES-128 암호화

HLS AES-128이란?

HLS AES-128은 Apple의 HTTP Live Streaming에서 사용하는 간단한 암호화 방식입니다. 완전한 DRM은 아니지만, 기본적인 콘텐츠 보호를 제공합니다.

HLS 암호화 매니페스트

다음은 m3u8를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
# AES-128 암호화 키 정보
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.bin",IV=0x12345678901234567890123456789012
# 세그먼트
#EXTINF:10.0,
segment-0.ts
#EXTINF:10.0,
segment-1.ts
#EXTINF:10.0,
segment-2.ts

HLS 암호화 생성 (FFmpeg)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 1. 암호화 키 생성 (16바이트)
openssl rand 16 > enc.key
# 2. IV 생성 (16바이트)
openssl rand -hex 16 > enc.iv
# 3. 키 정보 파일 생성
echo "https://example.com/key.bin" > enc.keyinfo
cat enc.key >> enc.keyinfo
cat enc.iv >> enc.keyinfo
# 4. FFmpeg로 암호화
ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -hls_time 10 \
  -hls_key_info_file enc.keyinfo \
  -hls_playlist_type vod \
  -hls_segment_filename "segment_%03d.ts" \
  playlist.m3u8
# 생성된 파일:
# - playlist.m3u8 (매니페스트)
# - segment_000.ts, segment_001.ts, ....(암호화된 세그먼트)

HLS 복호화 (Python)

다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

from Crypto.Cipher import AES
import requests
def decrypt_hls_segment(segment_url, key_url, iv):
    """HLS 세그먼트 복호화"""
    # 암호화 키 다운로드
    key_response = requests.get(key_url)
    key = key_response.content
    
    # 암호화된 세그먼트 다운로드
    segment_response = requests.get(segment_url)
    encrypted_data = segment_response.content
    
    # AES-128 CBC 복호화
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = cipher.decrypt(encrypted_data)
    
    # PKCS7 패딩 제거
    padding_length = decrypted_data[-1]
    decrypted_data = decrypted_data[:-padding_length]
    
    return decrypted_data
# 사용
iv = bytes.fromhex('12345678901234567890123456789012')
decrypted = decrypt_hls_segment(
    'https://cdn.example.com/segment_000.ts',
    'https://example.com/key.bin',
    iv
)
with open('decrypted_segment.ts', 'wb') as f:
    f.write(decrypted)

8. 실전 구현

멀티 DRM 플레이어

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class MultiDRMPlayer {
  constructor(videoElement) {
    this.video = videoElement;
    this.drmSystem = null;
  }
  
  async initialize(manifestUrl, licenseUrl, authToken) {
    // DRM 시스템 감지
    this.drmSystem = await this.detectDRM();
    console.log(`🔒 DRM System: ${this.drmSystem}`);
    
    // DRM 설정
    await this.setupDRM(licenseUrl, authToken);
    
    // 비디오 로드
    this.video.src = manifestUrl;
    await this.video.play();
  }
  
  async detectDRM() {
    // 플랫폼별 DRM 감지
    const ua = navigator.userAgent;
    
    if (/iPhone|iPad|iPod/.test(ua)) {
      return 'fairplay';
    } else if (/Android/.test(ua)) {
      return 'widevine';
    } else if (/Windows/.test(ua)) {
      // Edge는 PlayReady와 Widevine 모두 지원
      return 'widevine';  // 또는 'playready'
    } else if (/Mac/.test(ua)) {
      return 'fairplay';
    }
    
    return 'widevine';  // 기본값
  }
  
  async setupDRM(licenseUrl, authToken) {
    if (this.drmSystem === 'fairplay') {
      await this.setupFairPlay(licenseUrl, authToken);
    } else if (this.drmSystem === 'widevine') {
      await this.setupWidevine(licenseUrl, authToken);
    } else if (this.drmSystem === 'playready') {
      await this.setupPlayReady(licenseUrl, authToken);
    }
  }
  
  async setupWidevine(licenseUrl, authToken) {
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"',
        robustness: 'SW_SECURE_CRYPTO'
      }],
      audioCapabilities: [{
        contentType: 'audio/mp4; codecs="mp4a.40.2"',
        robustness: 'SW_SECURE_CRYPTO'
      }]
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    // 라이선스 요청 처리
    this.video.addEventListener('encrypted', (event) => {
      const session = mediaKeys.createSession();
      
      session.addEventListener('message', async (event) => {
        const response = await fetch(licenseUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/octet-stream',
            'Authorization': `Bearer ${authToken}`
          },
          body: event.message
        });
        
        const license = await response.arrayBuffer();
        await session.update(license);
      });
      
      session.generateRequest(event.initDataType, event.initData);
    });
    
    console.log('✅ Widevine initialized');
  }
  
  async setupFairPlay(licenseUrl, authToken) {
    // FairPlay는 네이티브 구현 필요
    console.log('ℹ️  FairPlay requires native implementation');
  }
  
  async setupPlayReady(licenseUrl, authToken) {
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"'
      }]
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.microsoft.playready',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    console.log('✅ PlayReady initialized');
  }
}
// 사용
const player = new MultiDRMPlayer(document.querySelector('video'));
player.initialize(
  'https://cdn.example.com/manifest.mpd',
  'https://license.example.com/widevine',
  'user-auth-token-123'
);

9. DRM 우회 방지

보안 레이어

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Layer1["레이어 1: 암호화"]
        Encrypt["AES-128/256\n콘텐츠 암호화"]
    end
    
    subgraph Layer2["레이어 2: 키 관리"]
        KeyRotation[키 로테이션]
        KeyDerivation[키 파생]
    end
    
    subgraph Layer3["레이어 3: 하드웨어 보안"]
        TEE["Trusted Execution\nEnvironment"]
        SecurePath[Secure Video Path]
    end
    
    subgraph Layer4["레이어 4: 애플리케이션"]
        Obfuscation[코드 난독화]
        AntiDebug[디버깅 방지]
        Watermark[워터마크]
    end
    
    subgraph Layer5["레이어 5: 네트워크"]
        TokenAuth[토큰 인증]
        GeoBlock[지역 차단]
        RateLimit[재생 횟수 제한]
    end
    
    Encrypt --> KeyRotation
    KeyRotation --> TEE
    TEE --> Obfuscation
    Obfuscation --> TokenAuth

워터마킹

다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

from PIL import Image, ImageDraw, ImageFont
import hashlib
def add_forensic_watermark(video_frame, user_id, timestamp):
    """포렌식 워터마크 추가"""
    img = Image.fromarray(video_frame)
    draw = ImageDraw.Draw(img)
    
    # 사용자 식별 정보 해시
    watermark_data = f"{user_id}:{timestamp}"
    watermark_hash = hashlib.sha256(watermark_data.encode()).hexdigest()[:16]
    
    # 보이지 않는 워터마크 (LSB 스테가노그래피)
    pixels = img.load()
    width, height = img.size
    
    for i, bit in enumerate(watermark_hash):
        if i >= width * height:
            break
        
        x = i % width
        y = i // width
        
        r, g, b = pixels[x, y]
        # LSB에 워터마크 비트 삽입
        r = (r & 0xFE) | (ord(bit) & 0x01)
        pixels[x, y] = (r, g, b)
    
    return img
# 워터마크 추출
def extract_watermark(video_frame):
    """워터마크 추출"""
    img = Image.fromarray(video_frame)
    pixels = img.load()
    width, height = img.size
    
    watermark_bits = []
    for i in range(16):  # 16자 해시
        x = i % width
        y = i // width
        r, g, b = pixels[x, y]
        watermark_bits.append(chr(r & 0x01))
    
    return '.join(watermark_bits)

화면 캡처 방지

다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 화면 캡처 감지 (완벽하지 않음)
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 화면 캡처 의심
    console.warn('⚠️ Screen capture suspected');
    video.pause();
  }
});
// 개발자 도구 감지
setInterval(() => {
  const before = new Date();
  debugger;  // 개발자 도구 열려있으면 멈춤
  const after = new Date();
  
  if (after - before > 100) {
    console.warn('⚠️ DevTools detected');
    video.pause();
  }
}, 1000);
// 우클릭 방지
video.addEventListener('contextmenu', (e) => {
  e.preventDefault();
});
// 드래그 방지
video.addEventListener('dragstart', (e) => {
  e.preventDefault();
});

실전 시나리오

시나리오 1: Netflix 스타일 DRM

다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

from flask import Flask, request, jsonify
import jwt
import time
import hashlib
app = Flask(__name__)
class DRMLicenseServer:
    def __init__(self):
        self.content_keys = {}  # content_id -> key
        self.user_licenses = {}  # user_id -> [content_ids]
    
    def generate_content_key(self, content_id):
        """콘텐츠 암호화 키 생성"""
        key = hashlib.sha256(f"{content_id}:{time.time()}".encode()).digest()[:16]
        self.content_keys[content_id] = key
        return key
    
    def verify_user_access(self, user_id, content_id):
        """사용자 접근 권한 확인"""
        # 구독 상태 확인
        subscription = get_user_subscription(user_id)
        if not subscription or not subscription.is_active():
            return False
        
        # 지역 제한 확인
        user_country = get_user_country(user_id)
        content_regions = get_content_regions(content_id)
        if user_country not in content_regions:
            return False
        
        # 동시 재생 제한 확인
        active_sessions = get_active_sessions(user_id)
        if len(active_sessions) >= subscription.max_streams:
            return False
        
        return True
    
    def issue_license(self, user_id, content_id, device_id):
        """라이선스 발급"""
        if not self.verify_user_access(user_id, content_id):
            return None
        
        # 라이선스 생성
        license_data = {
            'user_id': user_id,
            'content_id': content_id,
            'device_id': device_id,
            'issued_at': int(time.time()),
            'expires_at': int(time.time()) + 86400,  # 24시간
            'max_resolution': '1080p',
            'offline_allowed': False
        }
        
        # JWT로 서명
        license_token = jwt.encode(
            license_data,
            'secret-key',
            algorithm='HS256'
        )
        
        # 콘텐츠 키 (암호화되어 전달)
        content_key = self.content_keys.get(content_id)
        
        return {
            'license': license_token,
            'key': content_key.hex(),
            'expires_in': 86400
        }
@app.route('/license/widevine', methods=['POST'])
def widevine_license():
    # Widevine 라이선스 요청 처리
    challenge = request.data  # Widevine challenge
    auth_token = request.headers.get('Authorization')
    
    # 토큰 검증
    try:
        payload = jwt.decode(auth_token.replace('Bearer ', '), 'secret-key', algorithms=['HS256'])
        user_id = payload['user_id']
        content_id = payload['content_id']
    except:
        return jsonify({'error': 'Invalid token'}), 401
    
    # 라이선스 발급
    drm = DRMLicenseServer()
    license_data = drm.issue_license(user_id, content_id, request.remote_addr)
    
    if not license_data:
        return jsonify({'error': 'Access denied'}), 403
    
    # Widevine 라이선스 응답 (실제로는 Widevine SDK 사용)
    return license_data['key'], 200, {'Content-Type': 'application/octet-stream'}

시나리오 2: 오프라인 재생 (다운로드)

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 오프라인 DRM 콘텐츠 저장
class OfflineDRMPlayer {
  async downloadContent(manifestUrl, licenseUrl, authToken) {
    // Persistent License 요청
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"'
      }],
      persistentState: 'required',  // 오프라인 지원
      sessionTypes: ['persistent-license']
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    
    // Persistent Session 생성
    const session = mediaKeys.createSession('persistent-license');
    
    // 라이선스 요청
    session.addEventListener('message', async (event) => {
      const response = await fetch(licenseUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/octet-stream',
          'Authorization': `Bearer ${authToken}`
        },
        body: event.message
      });
      
      const license = await response.arrayBuffer();
      await session.update(license);
      
      // Session ID 저장 (나중에 재사용)
      const sessionId = session.sessionId;
      localStorage.setItem('drm-session-' + manifestUrl, sessionId);
      
      console.log('✅ Persistent license stored');
    });
    
    // 콘텐츠 다운로드 (Service Worker 사용)
    await this.downloadSegments(manifestUrl);
  }
  
  async playOffline(manifestUrl) {
    // 저장된 Session ID 로드
    const sessionId = localStorage.getItem('drm-session-' + manifestUrl);
    
    if (!sessionId) {
      throw new Error('No offline license found');
    }
    
    // MediaKeys 복원
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      [{ persistentState: 'required' }]
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    // Session 로드
    const session = mediaKeys.createSession('persistent-license');
    await session.load(sessionId);
    
    // 오프라인 콘텐츠 재생
    this.video.src = 'offline://' + manifestUrl;
    await this.video.play();
    
    console.log('✅ Playing offline content');
  }
  
  async downloadSegments(manifestUrl) {
    // Service Worker에서 세그먼트 다운로드 및 캐싱
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('download-' + manifestUrl);
  }
}

DRM 콘텐츠 패키징

Shaka Packager로 멀티 DRM

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#!/bin/bash
INPUT="input.mp4"
OUTPUT_DIR="output"
# Widevine + PlayReady + FairPlay 동시 지원
packager \
  in=$INPUT,stream=video,output=$OUTPUT_DIR/video.mp4 \
  in=$INPUT,stream=audio,output=$OUTPUT_DIR/audio.mp4 \
  --mpd_output $OUTPUT_DIR/manifest.mpd \
  --hls_master_playlist_output $OUTPUT_DIR/master.m3u8 \
  \
  --enable_widevine_encryption \
  --key_server_url "https://license.widevine.com/cenc/getcontentkey/widevine_test" \
  --content_id "content-123" \
  --signer "widevine_test" \
  --aes_signing_key "1ae8ccd0e7985cc0b6203a55855a1034afc252980e970ca90e5202689f947ab9" \
  --aes_signing_iv "d58ce954203b7c9a9a9d467f59839249" \
  \
  --enable_playready_encryption \
  --playready_server_url "https://playready.example.com/rightsmanager.asmx" \
  --playready_key_id "10000000100010001000100000000001" \
  --playready_key "3a2a1b68dd2b9d8b8d8d8d8d8d8d8d8d" \
  \
  --enable_fairplay_encryption \
  --fairplay_key_uri "skd://fairplay-key-123" \
  \
  --protection_scheme cenc \
  --clear_lead 3
echo "✅ Multi-DRM packaging complete"

DASH 매니페스트 (CENC)

다음은 xml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011">
  <Period>
    <AdaptationSet mimeType="video/mp4" codecs="avc1.42E01E">
      <!-- DRM 정보 -->
      <ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc"/>
      
      <!-- Widevine -->
      <ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
        <cenc:pssh>AAAAA...</cenc:pssh>
      </ContentProtection>
      
      <!-- PlayReady -->
      <ContentProtection schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
        <cenc:pssh>BBBBB...</cenc:pssh>
      </ContentProtection>
      
      <Representation bandwidth="1000000" width="1280" height="720">
        <SegmentTemplate media="video_$Number$.m4s" initialization="video_init.mp4"/>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

라이선스 서버 구현

Node.js 라이선스 서버

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' }));
// 콘텐츠 키 저장소
const contentKeys = new Map();
// 라이선스 발급
app.post('/license/:drm', async (req, res) => {
  const drmType = req.params.drm;  // widevine, fairplay, playready
  const challenge = req.body;
  const authToken = req.headers.authorization?.replace('Bearer ', ');
  
  try {
    // 토큰 검증
    const payload = jwt.verify(authToken, process.env.JWT_SECRET);
    const { userId, contentId, deviceId } = payload;
    
    // 권한 확인
    const hasAccess = await verifyUserAccess(userId, contentId);
    if (!hasAccess) {
      return res.status(403).json({ error: 'Access denied' });
    }
    
    // 콘텐츠 키 조회
    let contentKey = contentKeys.get(contentId);
    if (!contentKey) {
      // 새 키 생성
      contentKey = crypto.randomBytes(16);
      contentKeys.set(contentId, contentKey);
    }
    
    // DRM별 라이선스 생성
    let license;
    if (drmType === 'widevine') {
      license = await generateWidevineLicense(challenge, contentKey, userId);
    } else if (drmType === 'fairplay') {
      license = await generateFairPlayLicense(challenge, contentKey, userId);
    } else if (drmType === 'playready') {
      license = await generatePlayReadyLicense(challenge, contentKey, userId);
    }
    
    // 재생 로그
    await logPlayback(userId, contentId, deviceId, drmType);
    
    res.set('Content-Type', 'application/octet-stream');
    res.send(license);
    
    console.log(`✅ License issued: ${drmType} for user ${userId}`);
    
  } catch (err) {
    console.error('❌ License error:', err);
    res.status(401).json({ error: 'Invalid token' });
  }
});
async function verifyUserAccess(userId, contentId) {
  // 구독 확인
  const subscription = await getSubscription(userId);
  if (!subscription || !subscription.active) {
    return false;
  }
  
  // 지역 제한 확인
  const userCountry = await getUserCountry(userId);
  const contentRegions = await getContentRegions(contentId);
  if (!contentRegions.includes(userCountry)) {
    return false;
  }
  
  // 동시 재생 제한
  const activeSessions = await getActiveSessions(userId);
  if (activeSessions.length >= subscription.maxStreams) {
    return false;
  }
  
  return true;
}
async function generateWidevineLicense(challenge, contentKey, userId) {
  // 실제로는 Widevine SDK 사용
  // 여기서는 간소화된 예시
  
  // 1. Challenge 파싱
  // 2. 콘텐츠 키로 라이선스 생성
  // 3. 사용자별 제한 사항 추가 (해상도, 기간 등)
  
  const license = Buffer.from('widevine-license-data');
  return license;
}
app.listen(3000, () => {
  console.log('🔒 DRM License Server running on port 3000');
});

DRM 비교

기술적 비교

DRM개발사플랫폼보안 레벨최대 화질오프라인
Widevine L1GoogleAndroid, Chrome하드웨어4K
Widevine L3GoogleChrome, Firefox소프트웨어480p
FairPlayAppleiOS, Safari하드웨어4K
PlayReadyMicrosoftWindows, Xbox하드웨어4K
HLS AES-128Apple모든 플랫폼기본제한 없음
ClearKeyW3C모든 플랫폼없음제한 없음

서비스별 DRM 사용

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Netflix
        N_W[Widevine L1/L3]
        N_F[FairPlay]
        N_P[PlayReady]
    end
    
    subgraph YouTube_Premium
        Y_W[Widevine L3]
        Y_F[FairPlay]
    end
    
    subgraph Disney_Plus
        D_W[Widevine L1]
        D_F[FairPlay]
        D_P[PlayReady]
    end
    
    subgraph Spotify
        S_W[Widevine L3]
        S_F[FairPlay]
    end

DRM 우회 기법과 대응

일반적인 우회 시도

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    Attack[DRM 우회 시도]
    
    A1[화면 캡처]
    A2[메모리 덤프]
    A3[디버거 사용]
    A4[CDM 추출]
    A5[HDMI 캡처]
    
    Attack --> A1
    Attack --> A2
    Attack --> A3
    Attack --> A4
    Attack --> A5
    
    A1 --> D1[대응: 워터마크]
    A2 --> D2[대응: 메모리 암호화]
    A3 --> D3[대응: 안티 디버깅]
    A4 --> D4[대응: TEE/하드웨어]
    A5 --> D5[대응: HDCP]

HDCP (High-bandwidth Digital Content Protection)

아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

HDCP: 디지털 비디오 인터페이스 보호
HDMI/DisplayPort 연결에서 콘텐츠를 암호화하여
캡처 장비로 녹화를 방지
DRM L1 + HDCP → 완전한 보호 경로

안티 디버깅

다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 디버거 감지
(function() {
  let devtoolsOpen = false;
  const threshold = 160;
  
  setInterval(() => {
    if (window.outerWidth - window.innerWidth > threshold ||
        window.outerHeight - window.innerHeight > threshold) {
      if (!devtoolsOpen) {
        devtoolsOpen = true;
        console.log('⚠️ DevTools detected, pausing playback');
        document.querySelector('video')?.pause();
      }
    } else {
      devtoolsOpen = false;
    }
  }, 500);
})();
// 코드 난독화 (webpack)
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          mangle: true,
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    ]
  }
};

실전 플레이어 라이브러리

Shaka Player (Google)

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Shaka Player로 멀티 DRM 지원
import shaka from 'shaka-player';
const video = document.querySelector('video');
const player = new shaka.Player(video);
// DRM 설정
player.configure({
  drm: {
    servers: {
      'com.widevine.alpha': 'https://license.example.com/widevine',
      'com.microsoft.playready': 'https://license.example.com/playready'
    },
    advanced: {
      'com.widevine.alpha': {
        'videoRobustness': 'SW_SECURE_CRYPTO',
        'audioRobustness': 'SW_SECURE_CRYPTO'
      }
    }
  }
});
// 라이선스 요청 인터셉터
player.getNetworkingEngine().registerRequestFilter((type, request) => {
  if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
    // 인증 헤더 추가
    request.headers['Authorization'] = 'Bearer ' + getAuthToken();
  }
});
// 비디오 로드
player.load('https://cdn.example.com/manifest.mpd')
  .then(() => {
    console.log('✅ Video loaded');
    video.play();
  })
  .catch(err => {
    console.error('❌ Load error:', err);
  });

Video.js + videojs-contrib-eme

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

import videojs from 'video.js';
import 'videojs-contrib-eme';
const player = videojs('my-video', {
  plugins: {
    eme: {
      keySystems: {
        'com.widevine.alpha': {
          url: 'https://license.example.com/widevine',
          licenseHeaders: {
            'Authorization': 'Bearer ' + authToken
          },
          videoRobustness: 'SW_SECURE_CRYPTO',
          audioRobustness: 'SW_SECURE_CRYPTO'
        },
        'com.apple.fps.1_0': {
          certificateUri: 'https://example.com/fairplay.cer',
          licenseUri: 'https://license.example.com/fairplay',
          licenseHeaders: {
            'Authorization': 'Bearer ' + authToken
          }
        }
      }
    }
  }
});
player.src({
  src: 'https://cdn.example.com/manifest.mpd',
  type: 'application/dash+xml'
});
player.play();

DRM 테스트

Widevine 테스트 콘텐츠

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Google의 공개 테스트 스트림
const testStreams = {
  widevine: {
    manifest: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd',
    license: 'https://cwip-shaka-proxy.appspot.com/no_auth'
  },
  clearKey: {
    manifest: 'https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd',
    license: 'https://cwip-shaka-proxy.appspot.com/clearkey'
  }
};
// Shaka Player로 테스트
async function testDRM() {
  const player = new shaka.Player(video);
  
  player.configure({
    drm: {
      servers: {
        'com.widevine.alpha': testStreams.widevine.license
      }
    }
  });
  
  try {
    await player.load(testStreams.widevine.manifest);
    console.log('✅ Widevine test successful');
  } catch (err) {
    console.error('❌ Widevine test failed:', err);
  }
}

브라우저 DRM 지원 확인

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

async function checkDRMSupport() {
  const drmSystems = [
    { name: 'Widevine', keySystem: 'com.widevine.alpha' },
    { name: 'FairPlay', keySystem: 'com.apple.fps.1_0' },
    { name: 'PlayReady', keySystem: 'com.microsoft.playready' },
    { name: 'ClearKey', keySystem: 'org.w3.clearkey' }
  ];
  
  const results = {};
  
  for (const drm of drmSystems) {
    try {
      const config = [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"'
        }]
      }];
      
      await navigator.requestMediaKeySystemAccess(drm.keySystem, config);
      results[drm.name] = '✅ Supported';
    } catch (e) {
      results[drm.name] = '❌ Not supported';
    }
  }
  
  console.table(results);
  return results;
}
// 실행
checkDRMSupport();

성능 최적화

라이선스 캐싱

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class LicenseCache {
  constructor() {
    this.cache = new Map();
    this.maxAge = 3600 * 1000;  // 1시간
  }
  
  async getLicense(contentId, licenseUrl, authToken) {
    const cacheKey = `${contentId}:${authToken}`;
    const cached = this.cache.get(cacheKey);
    
    // 캐시 확인
    if (cached && Date.now() - cached.timestamp < this.maxAge) {
      console.log('✅ License from cache');
      return cached.license;
    }
    
    // 라이선스 요청
    const response = await fetch(licenseUrl, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });
    
    const license = await response.arrayBuffer();
    
    // 캐시 저장
    this.cache.set(cacheKey, {
      license,
      timestamp: Date.now()
    });
    
    console.log('✅ License from server');
    return license;
  }
  
  clear() {
    this.cache.clear();
  }
}
const licenseCache = new LicenseCache();

프리로딩

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 라이선스 미리 요청
async function preloadLicense(contentId, licenseUrl, authToken) {
  const config = [{
    initDataTypes: ['cenc'],
    videoCapabilities: [{
      contentType: 'video/mp4; codecs="avc1.42E01E"'
    }]
  }];
  
  const keySystemAccess = await navigator.requestMediaKeySystemAccess(
    'com.widevine.alpha',
    config
  );
  
  const mediaKeys = await keySystemAccess.createMediaKeys();
  const session = mediaKeys.createSession();
  
  // 미리 라이선스 요청
  session.addEventListener('message', async (event) => {
    const response = await fetch(licenseUrl, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${authToken}` },
      body: event.message
    });
    
    const license = await response.arrayBuffer();
    await session.update(license);
    
    console.log('✅ License preloaded');
  });
  
  // 더미 init data로 세션 생성
  const initData = new Uint8Array([/* PSSH box */]);
  await session.generateRequest('cenc', initData);
}

정리

DRM 선택 가이드

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TD
    Start[DRM 선택] --> Q1{플랫폼은?}
    
    Q1 -->|iOS/Safari| FairPlay[FairPlay]
    Q1 -->|Android/Chrome| Widevine[Widevine]
    Q1 -->|Windows/Xbox| PlayReady[PlayReady]
    Q1 -->|멀티 플랫폼| Multi["멀티 DRM\nCENC"]
    
    Multi --> Q2{보안 수준은?}
    Q2 -->|최고| L1["Widevine L1\n+ FairPlay\n+ PlayReady"]
    Q2 -->|중간| L3["Widevine L3\n+ FairPlay"]
    Q2 -->|기본| AES[HLS AES-128]

구현 복잡도

다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
복잡도: FairPlay > PlayReady > Widevine > HLS AES-128
FairPlay:
  - 인증서 발급 필요
  - 네이티브 구현 필수
  - Apple 승인 필요
Widevine:
  - 웹 표준 EME 사용
  - JavaScript로 구현 가능
  - Google 계약 필요
PlayReady:
  - Windows 플랫폼 통합
  - C# 구현 권장
  - Microsoft 계약 필요
HLS AES-128:
  - 간단한 구현
  - FFmpeg로 생성 가능
  - 계약 불필요

비용 비교

다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

라이선스 비용 (대략적):
Widevine:
  - 무료 (테스트)
  - 상용: 기기당 과금
FairPlay:
  - Apple Developer Program: $99/년
  - 추가 비용 없음
PlayReady:
  - 기기당 라이선스 비용
  - 볼륨 할인 가능
HLS AES-128:
  - 완전 무료
  - 오픈소스

베스트 프랙티스

1. 멀티 DRM 전략

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 플랫폼 감지 후 적절한 DRM 선택
function selectDRM() {
  const ua = navigator.userAgent;
  
  if (/iPhone|iPad|iPod/.test(ua)) {
    return {
      system: 'com.apple.fps.1_0',
      licenseUrl: 'https://license.example.com/fairplay'
    };
  } else if (/Android/.test(ua)) {
    return {
      system: 'com.widevine.alpha',
      licenseUrl: 'https://license.example.com/widevine'
    };
  } else if (/Windows/.test(ua)) {
    // PlayReady 또는 Widevine
    return {
      system: 'com.widevine.alpha',
      licenseUrl: 'https://license.example.com/widevine'
    };
  }
  
  return {
    system: 'com.widevine.alpha',
    licenseUrl: 'https://license.example.com/widevine'
  };
}

2. 에러 처리

다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

video.addEventListener('error', (event) => {
  const error = video.error;
  
  if (error.code === MediaError.MEDIA_ERR_ENCRYPTED) {
    console.error('❌ DRM error:', error.message);
    
    // 사용자에게 친화적인 메시지
    showError('이 콘텐츠를 재생할 수 없습니다. 브라우저를 업데이트하거나 다른 기기를 사용해주세요.');
  }
});
// MediaKeySession 에러
session.addEventListener('keystatuseschange', () => {
  session.keyStatuses.forEach((status, keyId) => {
    console.log(`Key ${keyId}: ${status}`);
    
    if (status === 'expired') {
      console.warn('⚠️ License expired, requesting new license');
      requestNewLicense();
    } else if (status === 'internal-error') {
      console.error('❌ DRM internal error');
    }
  });
});

3. 보안 체크리스트

다음은 python를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# DRM 구현 체크리스트
# ✅ 콘텐츠 암호화
# - AES-128 이상 사용
# - 키 로테이션 구현
# ✅ 라이선스 서버
# - HTTPS 필수
# - 토큰 기반 인증
# - 권한 검증 (구독, 지역, 기기)
# ✅ 클라이언트
# - EME API 사용
# - 에러 처리
# - 폴백 전략
# ✅ 모니터링
# - 재생 로그
# - 이상 패턴 감지
# - 라이선스 요청 빈도
# ✅ 추가 보호
# - 워터마크
# - 화면 캡처 방지
# - 안티 디버깅

참고 자료

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3