msid 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bf25c3ca34367f6073a84e93f267d835f34f56775664a23dc913d2143f9b5b61
4
+ data.tar.gz: 94ca6e504098733c75dd7017a42343d5ee41af696c9acc95f7b44824d9b15ba4
5
+ SHA512:
6
+ metadata.gz: 7652b3c1bce0ad68794440ce3a6d8a572d038ae38ef4c22e47af028e1a40f0aac148a969c9255e4dd7e1adec0605eaf84c76dcb9d305ba8346d93ab36f4fa952
7
+ data.tar.gz: 1675bb45cc39e7060bb3eecdd0ff64b1362b6761c0304e384759695a24b61a62cf078bd826a0d9e0a081475664403e924c832e6fa2daf18f03fefeaa6b4c8fa1
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in msid.gemspec
6
+ gemspec
7
+
8
+ gem 'minitest', '~> 5.0'
9
+ gem 'rake', '~> 13.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ msid (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.25.5)
10
+ rake (13.3.0)
11
+
12
+ PLATFORMS
13
+ arm64-darwin-24
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ minitest (~> 5.0)
18
+ msid!
19
+ rake (~> 13.0)
20
+
21
+ BUNDLED WITH
22
+ 2.6.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Davide Santangelo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # msid
2
+
3
+ `msid` is a Ruby gem for generating a unique and secure machine fingerprint ID. It gathers a wide range of system identifiers to create a hash that is highly specific to the machine it's run on.
4
+
5
+ This is useful for licensing, device identification, or any scenario where you need to reliably identify a specific machine.
6
+
7
+ ## How It Works
8
+
9
+ `msid` collects the following system information:
10
+ - Hostname
11
+ - All MAC addresses
12
+ - CPU model and core count
13
+ - Total RAM
14
+ - OS and kernel information
15
+ - Hardware Serial Number
16
+ - Hardware UUID
17
+ - Motherboard Serial Number
18
+ - Root disk volume UUID
19
+ - System Model Identifier
20
+ - GPU Information
21
+ - BIOS/Firmware Information
22
+ - Serial numbers of all physical disks
23
+
24
+ It then combines these components into a single string and uses the SHA-256 algorithm to produce a consistent, unique fingerprint.
25
+
26
+ ## Use Cases
27
+
28
+ Here are some practical scenarios where `msid` can be valuable:
29
+
30
+ ### Software Licensing
31
+ - **Offline License Activation**: Bind software licenses to specific machines without requiring constant internet validation.
32
+ - **Trial Period Management**: Track installations to prevent users from repeatedly installing trial versions.
33
+ - **License Auditing**: Keep track of which machines have your software installed for compliance purposes.
34
+
35
+ ### Security Applications
36
+ - **Multi-factor Authentication**: Add an extra layer of security by validating not just "something you know" (password) but also "something you have" (the specific device).
37
+ - **Session Security**: Detect when a user's session moves to a different machine, potentially indicating session hijacking.
38
+ - **Fraud Detection**: Flag accounts that suddenly access your service from multiple different machines in rapid succession.
39
+
40
+ ### System Management
41
+ - **Asset Tracking**: Inventory management for IT departments to track hardware across an organization.
42
+ - **Configuration Management**: Associate specific configurations or settings with particular machines.
43
+ - **Environment-specific Features**: Enable or disable features based on the hardware environment.
44
+
45
+ ### DevOps and Deployment
46
+ - **Environment Fingerprinting**: Distinguish between development, staging, and production environments.
47
+ - **Deployment Validation**: Ensure that deployments only happen from authorized build machines.
48
+ - **Infrastructure Auditing**: Track which servers or virtual machines are running your applications.
49
+
50
+ ### Usage Analytics
51
+ - **Device Demographics**: Gather anonymous statistics about what kinds of machines are running your software.
52
+ - **Installation Metrics**: Track unique installations versus reinstallations.
53
+ - **Hardware Compatibility**: Identify hardware configurations where your software performs well or poorly.
54
+
55
+ ## Installation
56
+
57
+ Add this line to your application's Gemfile:
58
+
59
+ ```ruby
60
+ gem 'msid'
61
+ ```
62
+
63
+ And then execute:
64
+
65
+ $ bundle install
66
+
67
+ Or install it yourself as:
68
+
69
+ $ gem install msid
70
+
71
+ ## Usage
72
+
73
+ ### From Ruby
74
+
75
+ You can generate the machine ID from your Ruby code.
76
+
77
+ ```ruby
78
+ require 'msid'
79
+
80
+ # Generate the default ID
81
+ machine_id = Msid.generate
82
+ puts machine_id
83
+ # => "a1b2c3d4..."
84
+ ```
85
+
86
+ #### Using a Salt
87
+
88
+ For enhanced security, you can provide a `salt`. This is useful if you want to generate different IDs for different applications on the same machine.
89
+
90
+ ```ruby
91
+ require 'msid'
92
+
93
+ # Generate an ID with a salt
94
+ app_specific_id = Msid.generate(salt: 'my-super-secret-app-key')
95
+ puts app_specific_id
96
+ # => "e5f6g7h8..." (different from the one without a salt)
97
+ ```
98
+
99
+ ### With IRB (for local development)
100
+
101
+ If you are developing the gem locally, you can test it with `irb`. From the root directory of the gem, run `irb` with the `-I` flag to add the `lib` directory to Ruby's load path.
102
+
103
+ ```sh
104
+ $ irb -I lib
105
+ ```
106
+
107
+ Then, inside `irb`, you can require and use the gem:
108
+
109
+ ```ruby
110
+ irb(main):001:0> require 'msid'
111
+ => true
112
+ irb(main):002:0> Msid.generate
113
+ => "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
114
+ ```
115
+
116
+ ### In a Rails Application
117
+
118
+ To use `msid` in a Rails application, add it to your `Gemfile`.
119
+
120
+ If you are developing the gem locally, you can use the `path` option:
121
+ ```ruby
122
+ # Gemfile
123
+ gem 'msid', path: '/path/to/your/msid/gem'
124
+ ```
125
+
126
+ If the gem is on RubyGems, add it normally:
127
+ ```ruby
128
+ # Gemfile
129
+ gem 'msid'
130
+ ```
131
+
132
+ Then run `bundle install`.
133
+
134
+ Now you can use it anywhere in your Rails application, including the Rails console:
135
+
136
+ ```sh
137
+ $ rails console
138
+ ```
139
+
140
+ ```ruby
141
+ # In Rails console
142
+ irb(main):001:0> Msid.generate
143
+ => "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
144
+
145
+ # For extra security, you can use a Rails secret as a salt
146
+ irb(main):002:0> Msid.generate(salt: Rails.application.credentials.secret_key_base)
147
+ => "f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2"
148
+ ```
149
+
150
+ ### From the Command Line
151
+
152
+ The gem provides a command-line executable to easily retrieve the machine ID.
153
+
154
+ ```sh
155
+ $ msid
156
+ a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
157
+ ```
158
+
159
+ ## Development
160
+
161
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
162
+
163
+ ## Contributing
164
+
165
+ Bug reports and pull requests are welcome on GitHub at https://github.com/davidesantangelo/msid.
166
+
167
+ ## License
168
+
169
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Msid
4
+ VERSION = '0.1.0'
5
+ end
data/lib/msid.rb ADDED
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'msid/version'
4
+ require 'socket'
5
+ require 'digest'
6
+ require 'open3'
7
+
8
+ module Msid
9
+ class Error < StandardError; end
10
+
11
+ # Generates a unique machine fingerprint ID.
12
+ #
13
+ # The fingerprint is created by collecting various system identifiers,
14
+ # combining them into a single string, and then hashing it with SHA-256.
15
+ # This makes the ID highly unique to the machine it's generated on.
16
+ class Generator
17
+ class << self
18
+ # Gathers a collection of machine-specific identifiers.
19
+ # @return [Array<String>] a list of identifiers.
20
+ def gather_components
21
+ [
22
+ hostname,
23
+ all_mac_addresses,
24
+ cpu_info,
25
+ cpu_cores,
26
+ total_memory,
27
+ os_info,
28
+ serial_number,
29
+ hardware_uuid,
30
+ baseboard_serial,
31
+ disk_uuid,
32
+ system_model,
33
+ gpu_info,
34
+ bios_info,
35
+ all_disk_serials
36
+ ].compact.reject(&:empty?)
37
+ end
38
+
39
+ # Generates the unique machine ID.
40
+ # @param salt [String, nil] An optional salt to add to the fingerprint.
41
+ # @return [String] The SHA-256 hash representing the machine ID.
42
+ def generate(salt: nil)
43
+ components = gather_components
44
+ raise Error, 'Could not gather any machine identifiers.' if components.empty?
45
+
46
+ components.push(salt.to_s) if salt
47
+
48
+ data_string = components.join(':')
49
+ Digest::SHA256.hexdigest(data_string)
50
+ end
51
+
52
+ private
53
+
54
+ # Executes a shell command and returns its stripped stdout.
55
+ # Returns nil if the command fails or an error occurs.
56
+ def run_command(command)
57
+ stdout, _stderr, status = Open3.capture3(command)
58
+ status.success? ? stdout.strip : nil
59
+ rescue StandardError
60
+ nil
61
+ end
62
+
63
+ # @return [String, nil] The machine's hostname.
64
+ def hostname
65
+ Socket.gethostname
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
70
+ # @return [String, nil] A sorted, concatenated string of all MAC addresses.
71
+ def all_mac_addresses
72
+ macs = case RUBY_PLATFORM
73
+ when /darwin/ # macOS
74
+ run_command("ifconfig -a | grep -o -E '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}'")&.split("\n")
75
+ when /linux/
76
+ # Read from /sys/class/net, which is more reliable than parsing `ip` command output
77
+ begin
78
+ Dir.glob('/sys/class/net/*/address').map { |f| File.read(f).strip }
79
+ rescue StandardError
80
+ []
81
+ end
82
+ when /mswin|mingw/ # Windows
83
+ run_command('getmac /v /fo csv | findstr /V "Disconnected"')
84
+ &.lines
85
+ &.map { |line| line.split(',')[2]&.gsub('"', '')&.strip }
86
+ else
87
+ []
88
+ end
89
+ macs&.compact&.reject(&:empty?)&.sort&.join(',')
90
+ end
91
+
92
+ # @return [String, nil] CPU model information.
93
+ def cpu_info
94
+ case RUBY_PLATFORM
95
+ when /darwin/
96
+ run_command('sysctl -n machdep.cpu.brand_string')
97
+ when /linux/
98
+ run_command("grep 'model name' /proc/cpuinfo | uniq | awk -F': ' '{print $2}'")
99
+ when /mswin|mingw/
100
+ run_command('wmic cpu get name /format:list | findstr "Name="')&.gsub('Name=', '')
101
+ end
102
+ end
103
+
104
+ # @return [String, nil] Number of CPU cores.
105
+ def cpu_cores
106
+ case RUBY_PLATFORM
107
+ when /darwin/
108
+ run_command('sysctl -n hw.ncpu')
109
+ when /linux/
110
+ run_command('nproc')
111
+ when /mswin|mingw/
112
+ run_command('wmic cpu get NumberOfCores | findstr /V "NumberOfCores"')
113
+ end
114
+ end
115
+
116
+ # @return [String, nil] Total physical memory in bytes or KB.
117
+ def total_memory
118
+ case RUBY_PLATFORM
119
+ when /darwin/
120
+ run_command('sysctl -n hw.memsize')
121
+ when /linux/
122
+ run_command("grep MemTotal /proc/meminfo | awk '{print $2}'")
123
+ when /mswin|mingw/
124
+ run_command('wmic ComputerSystem get TotalPhysicalMemory | findstr /V "TotalPhysicalMemory"')
125
+ end
126
+ end
127
+
128
+ # @return [String, nil] Operating system and kernel information.
129
+ def os_info
130
+ kernel_info = case RUBY_PLATFORM
131
+ when /mswin|mingw/
132
+ run_command('ver')
133
+ else
134
+ run_command('uname -r')
135
+ end
136
+ "#{RUBY_PLATFORM}-#{kernel_info}"
137
+ end
138
+
139
+ # @return [String, nil] The machine's hardware serial number.
140
+ def serial_number
141
+ case RUBY_PLATFORM
142
+ when /darwin/
143
+ run_command("system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}'")
144
+ when /linux/
145
+ run_command('cat /sys/class/dmi/id/product_serial')
146
+ when /mswin|mingw/
147
+ run_command('wmic bios get serialnumber | findstr /V "SerialNumber"')
148
+ end
149
+ end
150
+
151
+ # @return [String, nil] The machine's hardware UUID.
152
+ def hardware_uuid
153
+ case RUBY_PLATFORM
154
+ when /darwin/
155
+ run_command("system_profiler SPHardwareDataType | awk '/Hardware UUID/ {print $3}'")
156
+ when /linux/
157
+ run_command('cat /sys/class/dmi/id/product_uuid')
158
+ when /mswin|mingw/
159
+ run_command('wmic csproduct get uuid | findstr /V "UUID"')
160
+ end
161
+ end
162
+
163
+ # @return [String, nil] The machine's baseboard serial number.
164
+ def baseboard_serial
165
+ case RUBY_PLATFORM
166
+ when /darwin/
167
+ nil # Not easily available/distinct from system serial on macOS
168
+ when /linux/
169
+ run_command('cat /sys/class/dmi/id/board_serial')
170
+ when /mswin|mingw/
171
+ run_command('wmic baseboard get serialnumber | findstr /V "SerialNumber"')
172
+ end
173
+ end
174
+
175
+ # @return [String, nil] The UUID of the root volume.
176
+ def disk_uuid
177
+ case RUBY_PLATFORM
178
+ when /darwin/
179
+ run_command("diskutil info / | awk '/Volume UUID/{print $3}'")
180
+ when /linux/
181
+ device = run_command("df / | tail -n1 | awk '{print $1}'")
182
+ run_command("lsblk -no UUID #{device}") if device
183
+ when /mswin|mingw/
184
+ run_command('vol c: | findstr "Serial Number"')&.split&.last
185
+ end
186
+ end
187
+
188
+ # @return [String, nil] The machine's system model identifier.
189
+ def system_model
190
+ case RUBY_PLATFORM
191
+ when /darwin/
192
+ run_command("system_profiler SPHardwareDataType | awk '/Model Identifier/ {print $3}'")
193
+ when /linux/
194
+ run_command('cat /sys/class/dmi/id/product_name')
195
+ when /mswin|mingw/
196
+ run_command('wmic csproduct get name | findstr /V "Name"')
197
+ end
198
+ end
199
+
200
+ # @return [String, nil] The machine's GPU information.
201
+ def gpu_info
202
+ case RUBY_PLATFORM
203
+ when /darwin/
204
+ run_command("system_profiler SPDisplaysDataType | grep 'Chipset Model:' | awk -F': ' '{print $2}'")
205
+ when /linux/
206
+ run_command("lspci | grep -i 'vga\\|3d\\|2d' | head -n 1 | awk -F': ' '{print $3}'")
207
+ when /mswin|mingw/
208
+ run_command('wmic path win32_videocontroller get name | findstr /V "Name"')
209
+ end
210
+ end
211
+
212
+ # @return [String, nil] The machine's BIOS/firmware information.
213
+ def bios_info
214
+ case RUBY_PLATFORM
215
+ when /darwin/
216
+ run_command("system_profiler SPHardwareDataType | awk '/Boot ROM Version/ {print $4}'")
217
+ when /linux/
218
+ vendor = run_command('cat /sys/class/dmi/id/bios_vendor')
219
+ version = run_command('cat /sys/class/dmi/id/bios_version')
220
+ "#{vendor}-#{version}" if vendor && version
221
+ when /mswin|mingw/
222
+ run_command('wmic bios get manufacturer,version /format:list')
223
+ end
224
+ end
225
+
226
+ # @return [String, nil] A sorted, concatenated string of all physical disk serial numbers.
227
+ def all_disk_serials
228
+ serials = case RUBY_PLATFORM
229
+ when /darwin/
230
+ run_command("system_profiler SPStorageDataType | grep 'Serial Number:' | awk '{print $3}'")&.split("\n")
231
+ when /linux/
232
+ run_command('lsblk -d -o serial -n')&.split("\n")
233
+ when /mswin|mingw/
234
+ run_command('wmic diskdrive get serialnumber | findstr /V "SerialNumber"')&.split("\n")
235
+ else
236
+ []
237
+ end
238
+ serials&.compact&.map(&:strip)&.reject(&:empty?)&.sort&.join(',')
239
+ end
240
+ end
241
+ end
242
+
243
+ # Shortcut for Msid::Generator.generate
244
+ # @param salt [String, nil] An optional salt to add to the fingerprint.
245
+ def self.generate(salt: nil)
246
+ Generator.generate(salt: salt)
247
+ end
248
+ end
data/msid.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/msid/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'msid'
7
+ spec.version = Msid::VERSION
8
+ spec.authors = ['Davide Santangelo']
9
+ spec.email = ['']
10
+
11
+ spec.summary = 'Generates a unique machine fingerprint ID.'
12
+ spec.description = 'Creates a unique and secure machine fingerprint ID by gathering various system hardware and software identifiers. Designed to be difficult to replicate on another machine.'
13
+ spec.homepage = 'https://github.com/davidesantangelo/msid'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: msid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Davide Santangelo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-06-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Creates a unique and secure machine fingerprint ID by gathering various
14
+ system hardware and software identifiers. Designed to be difficult to replicate
15
+ on another machine.
16
+ email:
17
+ - ''
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - lib/msid.rb
28
+ - lib/msid/version.rb
29
+ - msid.gemspec
30
+ homepage: https://github.com/davidesantangelo/msid
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/davidesantangelo/msid
35
+ source_code_uri: https://github.com/davidesantangelo/msid
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.3.26
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Generates a unique machine fingerprint ID.
55
+ test_files: []