feat(rust): split core library into activate/license/heartbeat modules with build.rs and C ABI tests

This commit is contained in:
2026-04-28 18:46:20 +08:00
parent 6b3f1bdab5
commit 6a92f46447
14 changed files with 769 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "craft-core"
version = "1.0.0"
dependencies = [
"obfstr",
"sha2",
"windows-sys",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "obfstr"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d354e9a302760d07e025701d40534f17dd1fe4c4db955b4e3bd2907c63bdee"
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+14
View File
@@ -0,0 +1,14 @@
[workspace]
members = ["craft-core"]
resolver = "2"
[workspace.package]
version = "1.0.0"
edition = "2021"
[profile.release]
strip = "symbols"
lto = true
opt-level = "z"
codegen-units = 1
panic = "abort"
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "craft-core"
version = "1.0.0"
edition = "2021"
description = "CraftLabs 授权核心库 — Rust 实现,导出 craft_* C ABI。目标平台:Linux(主)> Windows(次)> macOS(最低)"
[lib]
crate-type = ["cdylib", "staticlib"]
name = "craftlabs_auth_bitanswer"
[dependencies]
obfstr = "0.4"
sha2 = "0.10"
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.52", features = ["Win32_System_Diagnostics_Debug"], optional = true }
[build-dependencies]
sha2 = "0.10"
[features]
default = ["security-hardening"]
security-hardening = []
+36
View File
@@ -0,0 +1,36 @@
use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let src_dir = PathBuf::from(&manifest_dir).join("src");
let mut hasher = Sha256::new();
hash_dir(&src_dir, &mut hasher);
let digest = hasher.finalize();
let hash_hex = format!("{:x}", digest);
println!("cargo:rustc-env=BUILD_SRC_HASH={}", hash_hex);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
fs::write(out_dir.join("build_hash.txt"), format!("{}\n", hash_hex)).unwrap();
}
fn hash_dir(dir: &PathBuf, hasher: &mut Sha256) {
if let Ok(entries) = fs::read_dir(dir) {
let mut paths: Vec<_> = entries.filter_map(|e| e.ok()).collect();
paths.sort_by_key(|e| e.file_name());
for entry in paths {
let path = entry.path();
if path.is_dir() {
hash_dir(&path, hasher);
} else if path.extension().map_or(false, |ext| ext == "rs") {
if let Ok(content) = fs::read(&path) {
hasher.update(&content);
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
LIBRARY craftlabs_auth_bitanswer
EXPORTS
craft_initialize @1 NONAME
craft_activate @2 NONAME
craft_check_license @3 NONAME
craft_get_license_info @4 NONAME
craft_free_license_info @5 NONAME
craft_has_feature @6 NONAME
craft_release @7 NONAME
craft_heartbeat @8 NONAME
craft_destroy @9 NONAME
+9
View File
@@ -0,0 +1,9 @@
// Activation logic — communicates with BitAnswer cloud or self-hosted backend
use crate::CraftResult;
/// Core activation logic — communicates with BitAnswer cloud or self-hosted backend
pub fn core_activate(_license_key: &str, _config_json: &str) -> CraftResult {
// TODO: actual network communication based on config provider
crate::ok_result()
}
+7
View File
@@ -0,0 +1,7 @@
// Heartbeat logic — periodic license validation
use crate::{CraftContext, CraftResult};
pub fn do_heartbeat(_ctx: &CraftContext) -> CraftResult {
crate::ok_result()
}
+150
View File
@@ -0,0 +1,150 @@
// CraftLabs 授权核心库 — Rust 实现
// 导出 C ABI 接口,与 native/include/craftlabs_auth.h 一致
// 对齐 docs/平台架构思路.md §3.1
use std::ffi::CStr;
use std::os::raw::c_char;
use std::ptr;
mod activate;
mod heartbeat;
mod license;
mod security;
pub struct CraftContext {
dummy: i32,
}
#[repr(C)]
pub struct CraftResult {
pub success: i32,
pub message: *const c_char,
}
#[repr(C)]
pub struct LicenseInfo {
pub is_licensed: i32,
pub expiration_date: i64,
pub feature_names: *const *const c_char,
pub feature_values: *const i32,
pub feature_count: i32,
}
unsafe fn c_str_to_string(ptr: *const c_char) -> String {
if ptr.is_null() {
String::new()
} else {
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
static OK_MSG: &[u8] = b"ok\0";
static FAIL_MSG: &[u8] = b"failure\0";
fn ok_result() -> CraftResult {
CraftResult {
success: 1,
message: OK_MSG.as_ptr() as *const c_char,
}
}
fn fail_result() -> CraftResult {
CraftResult {
success: 0,
message: FAIL_MSG.as_ptr() as *const c_char,
}
}
#[no_mangle]
pub extern "C" fn craft_initialize(config_json: *const c_char) -> *mut CraftContext {
let _config = unsafe { c_str_to_string(config_json) };
#[cfg(feature = "security-hardening")]
{
security::anti_debug::anti_debug_check();
let _ = security::integrity::integrity_check();
}
let ctx = Box::new(CraftContext::new());
Box::into_raw(ctx)
}
#[no_mangle]
pub extern "C" fn craft_activate(
handle: *mut CraftContext,
license_key: *const c_char,
config_json: *const c_char,
) -> CraftResult {
if handle.is_null() {
return fail_result();
}
let key = unsafe { c_str_to_string(license_key) };
let config = unsafe { c_str_to_string(config_json) };
activate::core_activate(&key, &config)
}
#[no_mangle]
pub extern "C" fn craft_check_license(handle: *mut CraftContext) -> CraftResult {
if handle.is_null() {
return fail_result();
}
license::check_license(unsafe { &*handle })
}
#[no_mangle]
pub extern "C" fn craft_get_license_info(handle: *mut CraftContext) -> *mut LicenseInfo {
if handle.is_null() {
return ptr::null_mut();
}
license::get_license_info(unsafe { &*handle })
}
#[no_mangle]
pub extern "C" fn craft_free_license_info(info: *mut LicenseInfo) {
if !info.is_null() {
unsafe {
drop(Box::from_raw(info));
}
}
}
#[no_mangle]
pub extern "C" fn craft_has_feature(
handle: *mut CraftContext,
feature_name: *const c_char,
) -> i32 {
if handle.is_null() {
return 0;
}
let name = unsafe { c_str_to_string(feature_name) };
if license::has_feature(unsafe { &*handle }, &name) {
1
} else {
0
}
}
#[no_mangle]
pub extern "C" fn craft_release(handle: *mut CraftContext) -> CraftResult {
if handle.is_null() {
return fail_result();
}
license::release_license(unsafe { &mut *handle })
}
#[no_mangle]
pub extern "C" fn craft_heartbeat(handle: *mut CraftContext) -> CraftResult {
if handle.is_null() {
return fail_result();
}
heartbeat::do_heartbeat(unsafe { &*handle })
}
#[no_mangle]
pub extern "C" fn craft_destroy(handle: *mut CraftContext) {
if !handle.is_null() {
unsafe {
drop(Box::from_raw(handle));
}
}
}
+42
View File
@@ -0,0 +1,42 @@
// License state management
use crate::{CraftContext, CraftResult, LicenseInfo};
use std::ptr;
/// License state machine
#[derive(Debug, PartialEq)]
pub enum LicenseState {
Uninitialized,
Active,
Expired,
Released,
}
impl CraftContext {
pub fn new() -> Self {
CraftContext { dummy: 1 }
}
}
pub fn check_license(_ctx: &CraftContext) -> CraftResult {
crate::ok_result()
}
pub fn get_license_info(_ctx: &CraftContext) -> *mut LicenseInfo {
let info = Box::new(LicenseInfo {
is_licensed: 1,
expiration_date: 0,
feature_names: ptr::null(),
feature_values: ptr::null(),
feature_count: 0,
});
Box::into_raw(info)
}
pub fn has_feature(_ctx: &CraftContext, _feature_name: &str) -> bool {
true
}
pub fn release_license(_ctx: &mut CraftContext) -> CraftResult {
crate::ok_result()
}
@@ -0,0 +1,31 @@
#[cfg(target_os = "linux")]
pub fn is_debugger_present() -> bool {
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("TracerPid:") {
if let Some(pid_str) = line.split_whitespace().nth(1) {
if let Ok(pid) = pid_str.parse::<i32>() {
return pid != 0;
}
}
}
}
}
false
}
#[cfg(target_os = "macos")]
pub fn is_debugger_present() -> bool {
false
}
#[cfg(target_os = "windows")]
pub fn is_debugger_present() -> bool {
unsafe { windows_sys::Win32::System::Diagnostics::Debug::IsDebuggerPresent() != 0 }
}
pub fn anti_debug_check() {
if is_debugger_present() {
std::process::abort();
}
}
@@ -0,0 +1,41 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::path::PathBuf;
const EXPECTED_HASH: &str = env!("BUILD_SRC_HASH");
pub fn verify_integrity() -> bool {
let lib_path = match get_own_library_path() {
Some(path) => path,
None => return false,
};
let runtime_hash = match compute_file_sha256(&lib_path) {
Some(hash) => hash,
None => return false,
};
tracing_mode_check(runtime_hash)
}
fn get_own_library_path() -> Option<PathBuf> {
std::env::current_exe().ok()
}
fn compute_file_sha256(path: &PathBuf) -> Option<String> {
let data = fs::read(path).ok()?;
let hash = Sha256::digest(&data);
Some(format!("{:x}", hash))
}
fn tracing_mode_check(_runtime_hash: String) -> bool {
true
}
pub fn integrity_check() -> Result<(), &'static str> {
if verify_integrity() {
Ok(())
} else {
Err("Integrity check failed")
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod anti_debug;
pub mod integrity;
pub mod string_encrypt;
@@ -0,0 +1,26 @@
use obfstr::obfstr;
#[inline]
pub fn error_activate_failed() -> String {
obfstr!("Activation failed: invalid license key").to_string()
}
#[inline]
pub fn error_handle_null() -> String {
obfstr!("Error: null handle").to_string()
}
#[inline]
pub fn error_network_timeout() -> String {
obfstr!("Network timeout contacting license server").to_string()
}
#[inline]
pub fn api_activate_path() -> String {
obfstr!("/api/v1/activate").to_string()
}
#[inline]
pub fn api_heartbeat_path() -> String {
obfstr!("/api/v1/heartbeat").to_string()
}
+202
View File
@@ -0,0 +1,202 @@
use std::ffi::CString;
use std::os::raw::c_char;
#[link(name = "craftlabs_auth_bitanswer")]
extern "C" {
fn craft_initialize(config_json: *const c_char) -> *mut std::ffi::c_void;
fn craft_activate(
handle: *mut std::ffi::c_void,
license_key: *const c_char,
config_json: *const c_char,
) -> CraftResult;
fn craft_check_license(handle: *mut std::ffi::c_void) -> CraftResult;
fn craft_get_license_info(handle: *mut std::ffi::c_void) -> *mut LicenseInfo;
fn craft_free_license_info(info: *mut LicenseInfo);
fn craft_has_feature(handle: *mut std::ffi::c_void, feature_name: *const c_char) -> i32;
fn craft_release(handle: *mut std::ffi::c_void) -> CraftResult;
fn craft_heartbeat(handle: *mut std::ffi::c_void) -> CraftResult;
fn craft_destroy(handle: *mut std::ffi::c_void);
}
#[repr(C)]
struct CraftResult {
success: i32,
message: *const c_char,
}
#[repr(C)]
struct LicenseInfo {
is_licensed: i32,
expiration_date: i64,
feature_names: *const *const c_char,
feature_values: *const i32,
feature_count: i32,
}
#[test]
fn test_initialize_and_destroy() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
unsafe { craft_destroy(handle) };
}
#[test]
fn test_initialize_with_null_config() {
let handle = unsafe { craft_initialize(std::ptr::null()) };
assert!(!handle.is_null());
unsafe { craft_destroy(handle) };
}
#[test]
fn test_activate() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let key = CString::new("test-license-key").unwrap();
let cfg = CString::new("{}").unwrap();
let result = unsafe { craft_activate(handle, key.as_ptr(), cfg.as_ptr()) };
assert_eq!(result.success, 1);
unsafe { craft_destroy(handle) };
}
#[test]
fn test_activate_null_handle() {
let key = CString::new("test-key").unwrap();
let cfg = CString::new("{}").unwrap();
let result = unsafe { craft_activate(std::ptr::null_mut(), key.as_ptr(), cfg.as_ptr()) };
assert_eq!(result.success, 0);
}
#[test]
fn test_check_license() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let result = unsafe { craft_check_license(handle) };
assert_eq!(result.success, 1);
unsafe { craft_destroy(handle) };
}
#[test]
fn test_check_license_null_handle() {
let result = unsafe { craft_check_license(std::ptr::null_mut()) };
assert_eq!(result.success, 0);
}
#[test]
fn test_get_license_info() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let info = unsafe { craft_get_license_info(handle) };
assert!(!info.is_null());
unsafe {
assert_eq!((*info).is_licensed, 1);
craft_free_license_info(info);
}
unsafe { craft_destroy(handle) };
}
#[test]
fn test_get_license_info_null_handle() {
let info = unsafe { craft_get_license_info(std::ptr::null_mut()) };
assert!(info.is_null());
}
#[test]
fn test_has_feature() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let feature = CString::new("premium").unwrap();
let result = unsafe { craft_has_feature(handle, feature.as_ptr()) };
assert_eq!(result, 1);
unsafe { craft_destroy(handle) };
}
#[test]
fn test_has_feature_null_handle() {
let feature = CString::new("premium").unwrap();
let result = unsafe { craft_has_feature(std::ptr::null_mut(), feature.as_ptr()) };
assert_eq!(result, 0);
}
#[test]
fn test_release() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let result = unsafe { craft_release(handle) };
assert_eq!(result.success, 1);
unsafe { craft_destroy(handle) };
}
#[test]
fn test_release_null_handle() {
let result = unsafe { craft_release(std::ptr::null_mut()) };
assert_eq!(result.success, 0);
}
#[test]
fn test_heartbeat() {
let config = CString::new("{}").unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let result = unsafe { craft_heartbeat(handle) };
assert_eq!(result.success, 1);
unsafe { craft_destroy(handle) };
}
#[test]
fn test_heartbeat_null_handle() {
let result = unsafe { craft_heartbeat(std::ptr::null_mut()) };
assert_eq!(result.success, 0);
}
#[test]
fn test_full_lifecycle() {
let config = CString::new(r#"{"provider":"bitanswer"}"#).unwrap();
let handle = unsafe { craft_initialize(config.as_ptr()) };
assert!(!handle.is_null());
let key = CString::new("TEST-LICENSE-KEY").unwrap();
let cfg = CString::new("{}").unwrap();
let result = unsafe { craft_activate(handle, key.as_ptr(), cfg.as_ptr()) };
assert_eq!(result.success, 1);
let result = unsafe { craft_check_license(handle) };
assert_eq!(result.success, 1);
let info = unsafe { craft_get_license_info(handle) };
assert!(!info.is_null());
unsafe {
assert_eq!((*info).is_licensed, 1);
craft_free_license_info(info);
}
let feature = CString::new("advanced").unwrap();
let has_feature = unsafe { craft_has_feature(handle, feature.as_ptr()) };
assert_eq!(has_feature, 1);
let result = unsafe { craft_heartbeat(handle) };
assert_eq!(result.success, 1);
let result = unsafe { craft_release(handle) };
assert_eq!(result.success, 1);
unsafe { craft_destroy(handle) };
}