SDK · 검증 가이드

라이선스 검증을
한 줄로 통합하세요.

X25519 키교환 · Ed25519 서버서명 · AES-256-GCM 기반 보안 채널을 자동으로 구성하는 클라이언트 SDK. 키 교환부터 하트비트, 키 회전까지 — 직접 다룰 일은 없습니다.

Version1.0.2Runtime.NET 8.0+TransportHTTPSHeartbeat20s

Licensify C# / .NET SDK 사용 가이드

Licensify 라이센스 인증을 앱에 붙이는 방법을 처음부터 끝까지 안내합니다. 복잡한 암호화·세션 관리는 SDK가 전부 처리하므로, 보통은 AuthenticateAsync 한 번이면 됩니다.


목차

  1. 설치
  2. 가장 간단한 예제
  3. 사용법 — 인증 · 정보 읽기 · 변수 저장 · 해제 · 세션 상실 · 자동 재인증 · 업데이트 · 에러 · 로깅
  4. 종합 예제
  5. API 레퍼런스
  6. 동작 방식과 제한
  7. 보안과 앱 보호

1. 설치

dotnet add package Licensify

요구 사항

  • .NET 8.0 이상
  • 의존성 BouncyCastle.Cryptography 2.4.0 (자동 설치됨)

2. 가장 간단한 예제

라이센스 키로 인증하고, 끝나면 해제합니다. 이게 전부입니다.

using Licensify;

await using var client = new LicenseClient(appId: "myapp", clientVersion: "1.0.0");

Session session = await client.AuthenticateAsync("사용자가-입력한-라이센스-키");

Console.WriteLine($"인증 성공! 남은 시간 {session.RemainingMillis} ms");

// await using 덕분에 스코프를 벗어날 때 세션이 자동으로 해제됩니다.

appId는 플랫폼에 등록한 소프트웨어 ID(최대 10자), clientVersion은 현재 앱 버전(최대 20자)입니다.


3. 사용법

3.1 인증하기

AuthenticateAsync는 서버 검증을 마치고 하트비트 루프를 자동 시작해 세션을 살아있게 유지합니다. 성공하면 Session을, 실패하면 LicenseException을 던집니다.

var client = new LicenseClient("myapp", "1.0.0");
Session session = await client.AuthenticateAsync(licenseKey);

if (client.IsActive)
    Console.WriteLine("세션 활성화됨");

참고

  • clientVersion은 서버에 등록되고 "사용 가능" 상태인 버전이어야 합니다. 아니면 UpdateRequired(SDK_006)가 납니다.
  • appId·clientVersion·licenseKey가 비어 있거나 길이를 초과하면 LicenseException이 아니라 ArgumentException 이 즉시 던져집니다(서버 호출 전). 즉, catch (LicenseException) 으로는 안 잡히니 입력값은 미리 검증하세요.

3.2 라이센스 정보 읽기

Console.WriteLine($"남은 시간(ms): {session.RemainingMillis}"); // 로컬에서 카운트다운, 0 이하로 안 내려감
Console.WriteLine($"만료 시각:     {session.Exp}");             // 표시용 문자열
Console.WriteLine($"최신 버전:     {session.LatestVersion}");

// 전역 변수(소프트웨어 공통) / 로컬 변수(이 라이센스 전용)
string theme = session.GlobalVariables.GetValueOrDefault("theme", "light");
string note  = session.LocalVariables.GetValueOrDefault("note", "");

3.3 로컬 변수 저장

라이센스별 데이터(설정, 점수 등)를 서버에 저장합니다. 저장된 값은 다음 인증 때 LocalVariables로 다시 내려옵니다(영속).

await session.SetLocalVariableAsync("highscore", "9001");
await session.SetLocalVariableAsync("note", "내 메모");

// 저장 성공 시 로컬 스냅샷에도 즉시 반영됩니다.
Console.WriteLine(session.LocalVariables["highscore"]); // "9001"

제한: key ≤ 50자, value ≤ 500자, 한 세션당 최대 30개. 초과 시 각각 VariableKeyTooLong / VariableValueTooLong / VariableLimitExceeded 예외.

참고: 위 제한 위반 같은 단순 검증 실패는 예외만 던지고 세션은 그대로 유지됩니다. 반면 저장 도중 세션 자체가 끝나는 치명 오류(SessionExpired·IntegrityError 등)가 나면, 예외가 던져지는 동시에 OnSessionLost 콜백도 함께 호출됩니다(아래 참고).

3.4 세션 해제 / 종료

작업이 끝나면 해제해 라이센스 좌석을 즉시 반납합니다. await using 또는 Dispose를 쓰면 자동입니다.

await client.ReleaseAsync();   // 명시적 해제 (best-effort, 여러 번 호출해도 안전)

// 또는 — 권장: using으로 자동 해제
await using (var c = new LicenseClient("myapp", "1.0.0"))
{
    var s = await c.AuthenticateAsync(licenseKey);
    // ...
} // 여기서 자동 해제

해제하지 못한 채 앱이 종료돼도 서버의 60초 세션 TTL이 좌석을 자동 정리합니다.

3.5 세션 상실 처리 (OnSessionLost)

세션 도중 하트비트 실패·만료·정지 등이 발생하면 호출됩니다. 보통 앱을 잠그거나 로그인 화면으로 보냅니다.

var options = new LicenseClientOptions
{
    OnSessionLost = ex =>
    {
        Console.WriteLine($"세션 종료됨: {ex.Code} - {ex.Message}");
        LockTheApp();
    }
};
var client = new LicenseClient("myapp", "1.0.0", options);

콜백은 백그라운드 스레드에서 호출될 수 있습니다. UI를 만진다면 디스패처로 넘기세요 (WPF: Dispatcher.BeginInvoke, WinForms: Invoke).

참고: 하트비트는 백그라운드에서 돌기 때문에, 그 실패로 인한 세션 상실은 try/catch로 잡을 수 없습니다 — OnSessionLost유일한 통로입니다. (예외로 잡히는 경우는 SetLocalVariableAsync 처럼 직접 await하는 호출이 세션-치명 오류를 만났을 때이며, 이때는 콜백과 예외가 둘 다 발생합니다.)

3.6 자동 재인증 (AutoReauthenticate)

일시적인 세션 끊김(SDK_301 등)에서 SDK가 1회 자동 재인증을 시도합니다. 그래도 실패하면 OnSessionLost가 호출됩니다. 정지·만료처럼 회복 불가한 경우엔 재시도하지 않습니다.

var options = new LicenseClientOptions
{
    AutoReauthenticate = true,
    OnSessionLost = ex => LockTheApp(),   // 자동 재인증도 실패한 뒤에만 호출됨
};

참고: 자동 재인증이 성공하면 세션은 조용히 이어지고 OnSessionLost는 호출되지 않습니다. 자동 재인증은 회복 가능한 경우(SessionExpired·IntegrityError)에만 1회 시도하며, 정지·만료(LicenseBanned·LicenseExpired)는 재시도해도 같은 결과라 곧장 OnSessionLost로 갑니다.

3.7 업데이트 필요 처리 (OnUpdateRequired)

서버가 현재 clientVersion을 더 이상 지원하지 않으면(SDK_006) 호출됩니다.

var options = new LicenseClientOptions
{
    OnUpdateRequired = info =>
        Console.WriteLine($"업데이트 필요: {info.LatestVersion} ({info.DownloadUrl})")
};

참고 — 콜백과 예외는 같은 사건의 두 통로입니다. SDK_006이 나면 이 콜백이 호출되는 동시에 AuthenticateAsyncUpdateRequired 예외도 던집니다(값은 동일: info.LatestVersion == ex.LatestVersion, info.DownloadUrl == ex.DownloadUrl). 즉 아래 3.8case LicenseErrorCode.UpdateRequired: 를 처리하든 안 하든 콜백은 그대로 호출됩니다. 둘 중 한 곳에서만 처리하면 됩니다 — 한 곳에 모아 안내하려면 콜백, 인증 흐름에서 바로 막으려면 catch.

모든 실패는 LicenseException 하나로 흐릅니다. Code로 분기하세요.

try
{
    var session = await client.AuthenticateAsync(licenseKey);
}
catch (LicenseException ex)
{
    switch (ex.Code)
    {
        case LicenseErrorCode.InvalidLicense:
            Console.WriteLine("라이센스 키가 올바르지 않습니다.");
            break;
        case LicenseErrorCode.LicenseBanned:
            Console.WriteLine($"정지된 라이센스입니다. 사유: {ex.Reason}, 해제: {ex.Until}");
            break;
        case LicenseErrorCode.UpdateRequired:
            Console.WriteLine($"업데이트가 필요합니다: {ex.LatestVersion}");
            break;
        case LicenseErrorCode.NetworkError when ex.Retryable:
            Console.WriteLine("네트워크 문제입니다. 잠시 후 다시 시도하세요.");
            break;
        default:
            Console.WriteLine($"[{ex.Code}] {ex.Message}");
            break;
    }
}

참고

  • Retryable은 "같은 요청을 나중에 다시 시도하면 성공할 수 있다"는 힌트일 뿐, SDK가 알아서 무한 재시도한다는 뜻이 아닙니다. (네트워크/5xx·VerifyInProgress처럼 SDK가 짧게 자동 재시도하는 경우는 있지만, 최종적으로 실패하면 예외로 올라옵니다.)
  • 앞서 말했듯 잘못된 인자(빈 값·길이 초과)는 LicenseException이 아니라 ArgumentException이라 이 catch에 잡히지 않습니다.

3.9 로깅

SDK 내부 동작을 들여다보고 싶을 때. 비밀값(키·라이센스)은 절대 로깅되지 않습니다.

var options = new LicenseClientOptions
{
    Logger = (level, message) => Console.WriteLine($"[{level}] {message}")
};

3.10 파일 무결성 검사 (FileHash) — 선택

서버에 등록한 바이너리 해시와 대조해 변조를 탐지합니다. 불일치 시 FileTampered 예외.

var options = new LicenseClientOptions { FileHash = sha256OfMyBinary }; // 최대 64자

4. 종합 예제

옵션·에러 처리·변수 저장·해제를 한 번에 보여주는 실전 예제입니다.

using Licensify;

public class LicenseManager
{
    private LicenseClient? _client;
    private Session? _session;

    public async Task<bool> LoginAsync(string licenseKey)
    {
        var options = new LicenseClientOptions
        {
            AutoReauthenticate = true,
            OnSessionLost     = OnSessionLost,
            OnUpdateRequired  = info => ShowUpdate(info.LatestVersion, info.DownloadUrl),
            Logger            = (lvl, msg) => Console.WriteLine($"[{lvl}] {msg}"),
            UserAgentSuffix   = "MyApp",
        };

        _client = new LicenseClient("myapp", "1.0.0", options);

        try
        {
            _session = await _client.AuthenticateAsync(licenseKey);
            Console.WriteLine($"인증 성공. 남은 시간 {_session.RemainingMillis} ms");

            // 저장해 둔 설정 읽기
            string theme = _session.LocalVariables.GetValueOrDefault("theme", "light");
            ApplyTheme(theme);
            return true;
        }
        catch (LicenseException ex)
        {
            Console.WriteLine($"인증 실패 [{ex.Code}]: {ex.Message}");
            await DisposeAsync();
            return false;
        }
    }

    public async Task SaveSettingAsync(string key, string value)
    {
        if (_session is null) return;
        try
        {
            await _session.SetLocalVariableAsync(key, value);
        }
        catch (LicenseException ex)
        {
            Console.WriteLine($"저장 실패 [{ex.Code}]: {ex.Message}");
        }
    }

    private void OnSessionLost(LicenseException ex)
    {
        // 백그라운드 스레드일 수 있음 — UI는 디스패처로
        Console.WriteLine($"세션 상실: {ex.Code}");
        LockTheApp();
    }

    public async Task DisposeAsync()
    {
        if (_client is not null)
            await _client.DisposeAsync(); // 세션 자동 해제
        _client = null;
        _session = null;
    }

    private void ApplyTheme(string theme) { /* ... */ }
    private void ShowUpdate(string v, string url) { /* ... */ }
    private void LockTheApp() { /* ... */ }
}

5. API 레퍼런스

LicenseClient

앱이 직접 다루는 단 하나의 진입점. 하나의 클라이언트는 동시에 최대 한 개의 세션을 가집니다 (재인증 시 이전 세션은 자동 해제).

멤버설명
LicenseClient(string appId, string clientVersion, LicenseClientOptions? options = null)생성자. appId ≤ 10자, clientVersion ≤ 20자. 서버 주소·검증 키는 SDK 내부 고정값입니다.
Task<Session> AuthenticateAsync(string licenseKey, CancellationToken ct = default)라이센스 인증 + 하트비트 시작. 성공 시 Session 반환, 실패 시 LicenseException.
Task ReleaseAsync(CancellationToken ct = default)세션 해제(좌석 반납). best-effort, 여러 번 호출해도 안전.
bool IsActive { get; }현재 세션이 살아있는지 여부.
ValueTask DisposeAsync() / void Dispose()해제 후 리소스 정리. await using 권장.

Session

인증된 세션에서 앱이 쓸 수 있는 정보만 노출합니다(암호 상태는 전부 숨김).

멤버타입설명
RemainingMillislong라이센스 만료까지 남은 밀리초. 로컬에서 카운트다운하며 0 미만이 되지 않음.
LocalVariablesIReadOnlyDictionary<string,string>이 라이센스 전용 변수(저장 시 영속).
GlobalVariablesIReadOnlyDictionary<string,string>소프트웨어 공통 변수.
LatestVersionstring서버가 알려준 최신 버전.
Expstring만료 시각(ISO 문자열, 표시 전용).
ServerTimestring서버 시각(표시 전용).
Task SetLocalVariableAsync(string key, string value, CancellationToken ct = default)로컬 변수 저장. 성공 시 로컬 스냅샷에도 반영.

LicenseClientOptions

선택 옵션. 보안 관련 값(서버 주소·키·암호 파라미터)은 SDK에 잠겨 있어 여기 없습니다.

옵션타입기본값설명
FileHashstring?null바이너리 무결성 해시(≤ 64자). 생략 시 검사 안 함.
AutoReauthenticateboolfalse세션 상실 시 1회 자동 재인증.
OnSessionLostAction<LicenseException>?null세션 상실 시 호출.
OnUpdateRequiredAction<UpdateInfo>?null버전 미지원(SDK_006) 시 호출.
LoggerAction<LogLevel, string>?null내부 로그 훅(비밀값 미포함).
UserAgentSuffixstring?nullUser-Agent에 덧붙일 앱 식별 문자열.

LicenseException

모든 실패가 흐르는 단일 타입.

멤버설명
CodeLicenseErrorCode — 분기 기준.
Retryable같은 요청을 재시도하면 성공할 수 있는지.
Context서버가 준 부가 정보(until, reason, latestVersion, downloadURL).
ServerCode원본 서버 코드(예: "SDK_102"), 로깅용.
Until / Reason / LatestVersion / DownloadUrlContext 편의 접근자.
Message사람이 읽을 수 있는 메시지.

UpdateInfo / LogLevel

public sealed record UpdateInfo(string LatestVersion, string DownloadUrl);
public enum LogLevel { Debug, Info, Warn, Error }

LicenseErrorCode 전체 목록

Code서버 코드의미재시도부가정보(Context)
NetworkError(없음)네트워크/TLS/타임아웃
IntegrityError(없음)복호화·서명·검증 실패
ServerErrorSDK_001 / SERVER_001서버 내부 오류
BadRequestSDK_002 / COMMON_001잘못된·변조된 요청
InvalidSoftwareSDK_003등록되지 않은 소프트웨어
InvalidLicenseSDK_004잘못된 라이센스
FileTamperedSDK_005파일 해시 불일치
UpdateRequiredSDK_006지원하지 않는 버전latestVersion, downloadURL
VerifyInProgressSDK_101인증 진행 중(잠시 후 재시도)
LicenseBannedSDK_102정지된 라이센스until, reason
LicenseExpiredSDK_103만료된 라이센스
LicenseActivationLimitSDK_104활성 한도 초과(첫 인증 거부)
SoftwareBannedSDK_201정지된 소프트웨어until, reason
SoftwareInactiveSDK_202비활성 소프트웨어
SoftwareMaintenanceSDK_204점검 중until, reason
SoftwareUnsupportedSDK_205지원 중단reason
SessionExpiredSDK_301세션 만료/없음
VariableInvalidSDK_401변수 key/value가 null
VariableKeyTooLongSDK_402key 길이 초과(> 50)
VariableValueTooLongSDK_403value 길이 초과(> 500)
VariableLimitExceededSDK_404변수 개수 초과(> 30)
Unknown(기타)매핑되지 않은 코드

6. 동작 방식과 제한

  • 하트비트: 인증 성공과 동시에 SDK가 약 20초 주기로 하트비트를 보내 세션을 유지합니다. 직접 호출할 필요 없습니다.
  • 세션 TTL 60초: 마지막 성공 하트비트로부터 60초가 지나면 서버가 세션을 정리합니다. 일시적 실패는 이 시간 안에서 자동 재시도됩니다.
  • 단일 세션: 한 클라이언트는 한 세션만 유지합니다. 다시 인증하면 이전 세션은 자동 해제됩니다.
  • 만료 판단: RemainingMillis는 인증 시점 값을 로컬에서 카운트다운한 것으로, 서버 시계와 무관하게 동작합니다(시간 로직에는 이 값을 사용하세요).
  • 로컬 변수 제한: key ≤ 50자, value ≤ 500자, 세션당 ≤ 30개.

7. 보안과 앱 보호

SDK가 보장하는 것 (통신 구간)

  • 서버는 자기만 아는 키로 응답에 서명하고, SDK가 내장된 공개키로 검증 → 가짜 서버·중간자(MITM) 차단
  • 매 세션 일회용 키로 AES-256-GCM 암호화 + 시퀀스 기반 재전송 방지
  • TLS 검증은 항상 켜져 있으며 끌 수 없습니다

⚠️ 앱 자체 보호는 개발자의 몫입니다

SDK는 통신을 안전하게 만들지만 여러분의 빌드된 앱을 보호하진 못합니다. 공격자가 여러분 바이너리에서 AuthenticateAsync 호출이나 그 결과를 검사하는 분기(if)·해시 검사 자체를 뜯어내면 SDK가 막을 길이 없습니다 — 그건 SDK가 아니라 여러분의 코드이기 때문입니다. 그래서:

  • 배포 바이너리는 반드시 난독화하세요
  • 인증·무결성 검사를 한 곳에 몰지 말고 여러 군데에 흩어 넣으세요
  • 검사 결과를 우회하기 어렵게 설계하세요