tusktsk 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'securerandom'
7
+ require 'time'
8
+ require 'fileutils'
9
+ require 'yaml'
10
+
11
+ module Tusk
12
+ # TuskLang SDK License Validation Module
13
+ # Enterprise-grade license validation for Ruby SDK
14
+ class License
15
+ attr_reader :license_key, :api_key, :session_id
16
+
17
+ def initialize(license_key, api_key, cache_dir = nil)
18
+ @license_key = license_key
19
+ @api_key = api_key
20
+ @session_id = SecureRandom.uuid
21
+ @license_cache = {}
22
+ @validation_history = []
23
+ @expiration_warnings = []
24
+
25
+ # Set up offline cache directory
26
+ @cache_dir = cache_dir || File.join(Dir.home, '.tusk', 'license_cache')
27
+ FileUtils.mkdir_p(@cache_dir)
28
+ @cache_file = File.join(@cache_dir, Digest::MD5.hexdigest(license_key) + '.cache')
29
+
30
+ # Load offline cache if exists
31
+ load_offline_cache
32
+
33
+ # Set up logger
34
+ @logger = Logger.new(STDERR)
35
+ @logger.progname = 'TuskLicense'
36
+ end
37
+
38
+ def validate_license_key
39
+ return { valid: false, error: 'Invalid license key format' } if @license_key.nil? || @license_key.length < 32
40
+ return { valid: false, error: 'Invalid license key prefix' } unless @license_key.start_with?('TUSK-')
41
+
42
+ checksum = Digest::SHA256.hexdigest(@license_key)
43
+ return { valid: false, error: 'Invalid license key checksum' } unless checksum.start_with?('tusk')
44
+
45
+ { valid: true, checksum: checksum }
46
+ rescue StandardError => e
47
+ { valid: false, error: e.message }
48
+ end
49
+
50
+ def verify_license_server(server_url = 'https://api.tusklang.org/v1/license')
51
+ uri = URI(server_url)
52
+
53
+ data = {
54
+ license_key: @license_key,
55
+ session_id: @session_id,
56
+ timestamp: Time.now.to_i
57
+ }
58
+
59
+ # Generate signature
60
+ signature = OpenSSL::HMAC.hexdigest('SHA256', @api_key, data.sort.to_h.to_json)
61
+ data[:signature] = signature
62
+
63
+ http = Net::HTTP.new(uri.host, uri.port)
64
+ http.use_ssl = true if uri.scheme == 'https'
65
+ http.open_timeout = 10
66
+ http.read_timeout = 10
67
+
68
+ request = Net::HTTP::Post.new(uri.path)
69
+ request['Authorization'] = "Bearer #{@api_key}"
70
+ request['Content-Type'] = 'application/json'
71
+ request['User-Agent'] = 'TuskLang-Ruby-SDK/1.0.0'
72
+ request.body = data.to_json
73
+
74
+ response = http.request(request)
75
+
76
+ if response.code == '200'
77
+ result = JSON.parse(response.body)
78
+
79
+ # Update in-memory cache
80
+ @license_cache[@license_key] = {
81
+ 'data' => result,
82
+ 'timestamp' => Time.now.to_i,
83
+ 'expires' => Time.now.to_i + 3600 # Cache for 1 hour
84
+ }
85
+
86
+ # Update offline cache
87
+ save_offline_cache(result)
88
+
89
+ result
90
+ else
91
+ @logger.warn("Server returned error: #{response.code}")
92
+ fallback_to_offline_cache("Server error: #{response.code}")
93
+ end
94
+
95
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
96
+ @logger.warn("Network timeout during license validation: #{e.message}")
97
+ fallback_to_offline_cache("Network timeout: #{e.message}")
98
+ rescue StandardError => e
99
+ @logger.error("Unexpected error during license validation: #{e.message}")
100
+ fallback_to_offline_cache(e.message)
101
+ end
102
+
103
+ def check_license_expiration
104
+ parts = @license_key.split('-')
105
+ return { expired: true, error: 'Invalid license key format' } if parts.length < 4
106
+
107
+ expiration_str = parts.last
108
+ expiration_timestamp = expiration_str.to_i(16)
109
+ expiration_date = Time.at(expiration_timestamp)
110
+ current_date = Time.now
111
+
112
+ if expiration_date < current_date
113
+ days_overdue = ((current_date - expiration_date) / 86400).to_i
114
+ return {
115
+ expired: true,
116
+ expiration_date: expiration_date.iso8601,
117
+ days_overdue: days_overdue
118
+ }
119
+ end
120
+
121
+ days_remaining = ((expiration_date - current_date) / 86400).to_i
122
+
123
+ if days_remaining <= 30
124
+ @expiration_warnings << {
125
+ timestamp: Time.now.to_i,
126
+ days_remaining: days_remaining
127
+ }
128
+ end
129
+
130
+ {
131
+ expired: false,
132
+ expiration_date: expiration_date.iso8601,
133
+ days_remaining: days_remaining,
134
+ warning: days_remaining <= 30
135
+ }
136
+
137
+ rescue StandardError => e
138
+ { expired: true, error: e.message }
139
+ end
140
+
141
+ def validate_license_permissions(feature)
142
+ # Check cached license data
143
+ if @license_cache.key?(@license_key)
144
+ cache_data = @license_cache[@license_key]
145
+ if Time.now.to_i < cache_data['expires']
146
+ license_data = cache_data['data']
147
+ if license_data['features']&.is_a?(Array)
148
+ if license_data['features'].include?(feature)
149
+ return { allowed: true, feature: feature }
150
+ else
151
+ return { allowed: false, feature: feature, error: 'Feature not licensed' }
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # Fallback to basic validation
158
+ case feature
159
+ when 'basic', 'core', 'standard'
160
+ { allowed: true, feature: feature }
161
+ when 'premium', 'enterprise'
162
+ if @license_key.upcase.include?('PREMIUM') || @license_key.upcase.include?('ENTERPRISE')
163
+ { allowed: true, feature: feature }
164
+ else
165
+ { allowed: false, feature: feature, error: 'Premium license required' }
166
+ end
167
+ else
168
+ { allowed: false, feature: feature, error: 'Unknown feature' }
169
+ end
170
+
171
+ rescue StandardError => e
172
+ { allowed: false, feature: feature, error: e.message }
173
+ end
174
+
175
+ def license_info
176
+ validation_result = validate_license_key
177
+ expiration_result = check_license_expiration
178
+
179
+ info = {
180
+ license_key: "#{@license_key[0..7]}...#{@license_key[-4..-1]}",
181
+ session_id: @session_id,
182
+ validation: validation_result,
183
+ expiration: expiration_result,
184
+ cache_status: @license_cache.key?(@license_key) ? 'cached' : 'not_cached',
185
+ validation_count: @validation_history.length,
186
+ warnings: @expiration_warnings.length
187
+ }
188
+
189
+ if @license_cache.key?(@license_key)
190
+ cache_data = @license_cache[@license_key]
191
+ info[:cached_data] = cache_data['data']
192
+ info[:cache_age] = Time.now.to_i - cache_data['timestamp']
193
+ end
194
+
195
+ info
196
+
197
+ rescue StandardError => e
198
+ { error: e.message }
199
+ end
200
+
201
+ def refresh_license_cache
202
+ @license_cache.delete(@license_key)
203
+ verify_license_server
204
+ end
205
+
206
+ def log_validation_attempt(success, details = '')
207
+ @validation_history << {
208
+ timestamp: Time.now.to_i,
209
+ success: success,
210
+ details: details,
211
+ session_id: @session_id
212
+ }
213
+ end
214
+
215
+ def validation_history
216
+ @validation_history
217
+ end
218
+
219
+ def clear_validation_history
220
+ @validation_history.clear
221
+ end
222
+
223
+ private
224
+
225
+ def load_offline_cache
226
+ return unless File.exist?(@cache_file)
227
+
228
+ cached_data = YAML.load_file(@cache_file)
229
+
230
+ # Verify the cache is for the correct license key
231
+ key_hash = Digest::SHA256.hexdigest(@license_key)
232
+ if cached_data['license_key_hash'] == key_hash
233
+ @offline_cache = cached_data
234
+ @logger.info('Loaded offline license cache')
235
+ else
236
+ @offline_cache = nil
237
+ @logger.warn('Offline cache key mismatch')
238
+ end
239
+ rescue StandardError => e
240
+ @logger.error("Failed to load offline cache: #{e.message}")
241
+ @offline_cache = nil
242
+ end
243
+
244
+ def save_offline_cache(license_data)
245
+ cache_data = {
246
+ 'license_key_hash' => Digest::SHA256.hexdigest(@license_key),
247
+ 'license_data' => license_data,
248
+ 'timestamp' => Time.now.to_i,
249
+ 'expiration' => check_license_expiration
250
+ }
251
+
252
+ File.write(@cache_file, cache_data.to_yaml)
253
+ @offline_cache = cache_data
254
+ @logger.info('Saved license data to offline cache')
255
+ rescue StandardError => e
256
+ @logger.error("Failed to save offline cache: #{e.message}")
257
+ end
258
+
259
+ def fallback_to_offline_cache(error_msg)
260
+ if @offline_cache && @offline_cache['license_data']
261
+ cache_age = Time.now.to_i - @offline_cache['timestamp']
262
+ cache_age_days = cache_age.to_f / 86400
263
+
264
+ # Check if cached license is not expired
265
+ expiration = @offline_cache['expiration'] || {}
266
+ unless expiration['expired']
267
+ @logger.warn("Using offline license cache (age: #{'%.1f' % cache_age_days} days)")
268
+ return @offline_cache['license_data'].merge(
269
+ 'offline_mode' => true,
270
+ 'cache_age_days' => cache_age_days,
271
+ 'warning' => "Operating in offline mode due to: #{error_msg}"
272
+ )
273
+ else
274
+ return {
275
+ 'valid' => false,
276
+ 'error' => "License expired and server unreachable: #{error_msg}",
277
+ 'offline_cache_expired' => true
278
+ }
279
+ end
280
+ else
281
+ {
282
+ 'valid' => false,
283
+ 'error' => "No offline cache available: #{error_msg}",
284
+ 'offline_cache_missing' => true
285
+ }
286
+ end
287
+ end
288
+ end
289
+
290
+ # Global license instance
291
+ @license_instance = nil
292
+
293
+ class << self
294
+ def initialize_license(license_key, api_key, cache_dir = nil)
295
+ @license_instance = License.new(license_key, api_key, cache_dir)
296
+ end
297
+
298
+ def license
299
+ raise 'License not initialized. Call Tusk.initialize_license() first.' unless @license_instance
300
+ @license_instance
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'json'
6
+ require 'securerandom'
7
+
8
+ module Tusk
9
+ # TuskLang SDK Protection Core Module
10
+ # Enterprise-grade protection for Ruby SDK
11
+ class Protection
12
+ attr_reader :license_key, :api_key, :session_id, :integrity_checks, :usage_metrics
13
+
14
+ def initialize(license_key, api_key)
15
+ @license_key = license_key
16
+ @api_key = api_key
17
+ @session_id = SecureRandom.uuid
18
+ @encryption_key = derive_key(license_key)
19
+ @integrity_checks = {}
20
+ @usage_metrics = UsageMetrics.new
21
+ end
22
+
23
+ def validate_license
24
+ return false if license_key.nil? || license_key.length < 32
25
+
26
+ checksum = OpenSSL::Digest::SHA256.hexdigest(license_key)
27
+ checksum.start_with?('tusk')
28
+ rescue StandardError
29
+ false
30
+ end
31
+
32
+ def encrypt_data(data)
33
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
34
+ cipher.encrypt
35
+ cipher.key = @encryption_key
36
+ iv = cipher.random_iv
37
+
38
+ encrypted = cipher.update(data) + cipher.final
39
+ auth_tag = cipher.auth_tag
40
+
41
+ result = iv + auth_tag + encrypted
42
+ Base64.strict_encode64(result)
43
+ rescue StandardError
44
+ data
45
+ end
46
+
47
+ def decrypt_data(encrypted_data)
48
+ decoded = Base64.strict_decode64(encrypted_data)
49
+ return encrypted_data if decoded.length < 28
50
+
51
+ iv = decoded[0, 12]
52
+ auth_tag = decoded[12, 16]
53
+ encrypted = decoded[28..-1]
54
+
55
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
56
+ cipher.decrypt
57
+ cipher.key = @encryption_key
58
+ cipher.iv = iv
59
+ cipher.auth_tag = auth_tag
60
+ cipher.auth_data = ''
61
+
62
+ cipher.update(encrypted) + cipher.final
63
+ rescue StandardError
64
+ encrypted_data
65
+ end
66
+
67
+ def verify_integrity(data, signature)
68
+ expected_signature = generate_signature(data)
69
+ signature == expected_signature
70
+ rescue StandardError
71
+ false
72
+ end
73
+
74
+ def generate_signature(data)
75
+ OpenSSL::HMAC.hexdigest('SHA256', @api_key, data)
76
+ end
77
+
78
+ def track_usage(operation, success = true)
79
+ @usage_metrics.increment_api_calls
80
+ @usage_metrics.increment_errors unless success
81
+ end
82
+
83
+ def get_metrics
84
+ {
85
+ start_time: @usage_metrics.start_time,
86
+ api_calls: @usage_metrics.api_calls,
87
+ errors: @usage_metrics.errors,
88
+ session_id: @session_id,
89
+ uptime: Time.now.to_i - @usage_metrics.start_time
90
+ }
91
+ end
92
+
93
+ def obfuscate_code(code)
94
+ Base64.strict_encode64(code)
95
+ end
96
+
97
+ def detect_tampering
98
+ # In production, implement file integrity checks
99
+ # For now, return true as placeholder
100
+ true
101
+ rescue StandardError
102
+ false
103
+ end
104
+
105
+ def report_violation(violation_type, details)
106
+ violation = Violation.new(
107
+ Time.now.to_i,
108
+ @session_id,
109
+ violation_type,
110
+ details,
111
+ @license_key[0, 8] + '...'
112
+ )
113
+
114
+ puts "SECURITY VIOLATION: #{violation}"
115
+ violation
116
+ end
117
+
118
+ private
119
+
120
+ def derive_key(password)
121
+ salt = 'tusklang_protection_salt'
122
+ OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 100_000, 32)
123
+ end
124
+
125
+ # Inner classes
126
+ class UsageMetrics
127
+ attr_reader :start_time, :api_calls, :errors
128
+
129
+ def initialize
130
+ @start_time = Time.now.to_i
131
+ @api_calls = 0
132
+ @errors = 0
133
+ @mutex = Mutex.new
134
+ end
135
+
136
+ def increment_api_calls
137
+ @mutex.synchronize { @api_calls += 1 }
138
+ end
139
+
140
+ def increment_errors
141
+ @mutex.synchronize { @errors += 1 }
142
+ end
143
+ end
144
+
145
+ class Violation
146
+ attr_reader :timestamp, :session_id, :violation_type, :details, :license_key_partial
147
+
148
+ def initialize(timestamp, session_id, violation_type, details, license_key_partial)
149
+ @timestamp = timestamp
150
+ @session_id = session_id
151
+ @violation_type = violation_type
152
+ @details = details
153
+ @license_key_partial = license_key_partial
154
+ end
155
+
156
+ def to_s
157
+ "Violation{timestamp=#{@timestamp}, session_id='#{@session_id}', type='#{@violation_type}', details='#{@details}', license_key_partial='#{@license_key_partial}'}"
158
+ end
159
+ end
160
+
161
+ # Global protection instance
162
+ @protection_instance = nil
163
+ @instance_mutex = Mutex.new
164
+
165
+ class << self
166
+ def initialize_protection(license_key, api_key)
167
+ @instance_mutex.synchronize do
168
+ @protection_instance = new(license_key, api_key)
169
+ end
170
+ end
171
+
172
+ def get_protection
173
+ if @protection_instance.nil?
174
+ raise RuntimeError, 'Protection not initialized. Call initialize_protection() first.'
175
+ end
176
+ @protection_instance
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shell Storage for Ruby
4
+ # Handles binary data compression and storage
5
+
6
+ module TuskLang
7
+ # Shell format storage for binary data
8
+ class ShellStorage
9
+ # Magic bytes for Shell format identification
10
+ MAGIC = 'FLEX'.bytes
11
+ VERSION = 1
12
+
13
+ # Pack data into Shell binary format
14
+ def self.pack(data)
15
+ # Compress data if it's not already compressed
16
+ compressed_data = if data[:data].is_a?(String)
17
+ Zlib::Deflate.deflate(data[:data])
18
+ elsif data[:data].is_a?(Array)
19
+ Zlib::Deflate.deflate(data[:data].pack('C*'))
20
+ else
21
+ Zlib::Deflate.deflate(data[:data].to_s)
22
+ end
23
+
24
+ id_bytes = data[:id].bytes
25
+
26
+ # Build binary format
27
+ # Magic (4) + Version (1) + ID Length (4) + ID + Data Length (4) + Data
28
+ result = []
29
+ result.concat(MAGIC)
30
+ result << VERSION
31
+ result.concat([id_bytes.length].pack('>N').bytes)
32
+ result.concat(id_bytes)
33
+ result.concat([compressed_data.length].pack('>N').bytes)
34
+ result.concat(compressed_data.bytes)
35
+
36
+ result.pack('C*')
37
+ end
38
+
39
+ # Unpack Shell binary format
40
+ def self.unpack(shell_data)
41
+ offset = 0
42
+
43
+ # Check magic bytes
44
+ magic = shell_data.bytes[offset, 4]
45
+ raise ArgumentError, 'Invalid shell format' unless magic == MAGIC
46
+ offset += 4
47
+
48
+ # Read version
49
+ version = shell_data.bytes[offset]
50
+ raise ArgumentError, "Unsupported shell version: #{version}" unless version == VERSION
51
+ offset += 1
52
+
53
+ # Read ID
54
+ id_length = shell_data.bytes[offset, 4].pack('C*').unpack('>N')[0]
55
+ offset += 4
56
+ storage_id = shell_data.bytes[offset, id_length].pack('C*').force_encoding('UTF-8')
57
+ offset += id_length
58
+
59
+ # Read data
60
+ data_length = shell_data.bytes[offset, 4].pack('C*').unpack('>N')[0]
61
+ offset += 4
62
+ compressed_data = shell_data.bytes[offset, data_length].pack('C*')
63
+
64
+ # Decompress
65
+ data = Zlib::Inflate.inflate(compressed_data).force_encoding('UTF-8')
66
+
67
+ {
68
+ version: version,
69
+ id: storage_id,
70
+ data: data
71
+ }
72
+ end
73
+
74
+ # Detect the type of data stored in Shell format
75
+ def self.detect_type(shell_data)
76
+ data = unpack(shell_data)
77
+ content = data[:data].to_s
78
+
79
+ # Try to detect JSON
80
+ return 'json' if content.start_with?('{') || content.start_with?('[')
81
+
82
+ # Try to detect TSK format
83
+ return 'tsk' if content.include?('[') && content.include?(']') && content.include?('=')
84
+
85
+ # Try to detect XML
86
+ return 'xml' if content.start_with?('<') && content.include?('>')
87
+
88
+ # Default to text
89
+ 'text'
90
+ rescue
91
+ 'unknown'
92
+ end
93
+
94
+ # Create Shell data with metadata
95
+ def self.create_shell_data(data, id, metadata = nil)
96
+ {
97
+ version: VERSION,
98
+ id: id,
99
+ data: data,
100
+ metadata: metadata || {}
101
+ }
102
+ end
103
+ end
104
+ end