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
+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) };
}