From 00000000f496a8ca5f8e3374eae679b393285274 Mon Sep 17 00:00:00 2001 From: Lakr Aream <25259084+Lakr233@users.noreply.github.com> Date: Mon, 24 Jan 2022 16:39:53 +0800 Subject: [PATCH] Initial commit --- .gitignore | 56 ++++++ LICENSE | 13 ++ README.md | 2 + generate_keys.rb | 10 + generate_licenses.rb | 432 ++++++++++++++++++++++++++++++++++++++++++ license_key | 27 +++ license_key.pub | 9 + make.sh | 10 + result.gitlab-license | 22 +++ 9 files changed, 581 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 generate_keys.rb create mode 100644 generate_licenses.rb create mode 100644 license_key create mode 100644 license_key.pub create mode 100755 make.sh create mode 100644 result.gitlab-license diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e3200e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Used by dotenv library to load environment variables. +# .env + +# Ignore Byebug command history file. +.byebug_history + +## Specific to RubyMotion: +.dat* +.repl_history +build/ +*.bridgesupport +build-iPhoneOS/ +build-iPhoneSimulator/ + +## Specific to RubyMotion (use of CocoaPods): +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# vendor/Pods/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# Used by RuboCop. Remote config files pulled in from inherit_from directive. +# .rubocop-https?--* diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..07b7a81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..5aae44c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# GitLab-License-Generator +Generator GitLab License For Self-Hosted/Private Instances diff --git a/generate_keys.rb b/generate_keys.rb new file mode 100644 index 000000000..197f2ad --- /dev/null +++ b/generate_keys.rb @@ -0,0 +1,10 @@ +require 'openssl' + +key_pair = OpenSSL::PKey::RSA.generate(2048) +File.open("license_key", "w") { |f| f.write(key_pair.to_pem) } + +public_key = key_pair.public_key +File.open("license_key.pub", "w") { |f| f.write(public_key.to_pem) } + +puts "Generated RSA key pairs, use generate_licenses.rb to generate licenses." +puts "Make your own customization to the code if needed." \ No newline at end of file diff --git a/generate_licenses.rb b/generate_licenses.rb new file mode 100644 index 000000000..c9aae84 --- /dev/null +++ b/generate_licenses.rb @@ -0,0 +1,432 @@ +require 'openssl' +require 'date' +require 'json' +require 'base64' + +module Gitlab + class License + VERSION = '2.1.0'.freeze + end +end + +module Gitlab + class License + class Encryptor + class Error < StandardError; end + class KeyError < Error; end + class DecryptionError < Error; end + + attr_accessor :key + + def initialize(key) + raise KeyError, 'No RSA encryption key provided.' if key && !key.is_a?(OpenSSL::PKey::RSA) + + @key = key + end + + def encrypt(data) + raise KeyError, 'Provided key is not a private key.' unless key.private? + + # Encrypt the data using symmetric AES encryption. + cipher = OpenSSL::Cipher::AES128.new(:CBC) + cipher.encrypt + aes_key = cipher.random_key + aes_iv = cipher.random_iv + + encrypted_data = cipher.update(data) + cipher.final + + # Encrypt the AES key using asymmetric RSA encryption. + encrypted_key = key.private_encrypt(aes_key) + + encryption_data = { + 'data' => Base64.encode64(encrypted_data), + 'key' => Base64.encode64(encrypted_key), + 'iv' => Base64.encode64(aes_iv) + } + + json_data = JSON.dump(encryption_data) + Base64.encode64(json_data) + end + + def decrypt(data) + raise KeyError, 'Provided key is not a public key.' unless key.public? + + json_data = Base64.decode64(data.chomp) + + begin + encryption_data = JSON.parse(json_data) + rescue JSON::ParserError + raise DecryptionError, 'Encryption data is invalid JSON.' + end + + unless %w[data key iv].all? { |key| encryption_data[key] } + raise DecryptionError, 'Required field missing from encryption data.' + end + + encrypted_data = Base64.decode64(encryption_data['data']) + encrypted_key = Base64.decode64(encryption_data['key']) + aes_iv = Base64.decode64(encryption_data['iv']) + + begin + # Decrypt the AES key using asymmetric RSA encryption. + aes_key = self.key.public_decrypt(encrypted_key) + rescue OpenSSL::PKey::RSAError + raise DecryptionError, 'AES encryption key could not be decrypted.' + end + + # Decrypt the data using symmetric AES encryption. + cipher = OpenSSL::Cipher::AES128.new(:CBC) + cipher.decrypt + + begin + cipher.key = aes_key + rescue OpenSSL::Cipher::CipherError + raise DecryptionError, 'AES encryption key is invalid.' + end + + begin + cipher.iv = aes_iv + rescue OpenSSL::Cipher::CipherError + raise DecryptionError, 'AES IV is invalid.' + end + + begin + data = cipher.update(encrypted_data) + cipher.final + rescue OpenSSL::Cipher::CipherError + raise DecryptionError, 'Data could not be decrypted.' + end + + data + end + end + end +end + +module Gitlab + class License + module Boundary + BOUNDARY_START = /(\A|\r?\n)-*BEGIN .+? LICENSE-*\r?\n/.freeze + BOUNDARY_END = /\r?\n-*END .+? LICENSE-*(\r?\n|\z)/.freeze + + class << self + def add_boundary(data, product_name) + data = remove_boundary(data) + + product_name.upcase! + + pad = lambda do |message, width| + total_padding = [width - message.length, 0].max + + padding = total_padding / 2.0 + [ + '-' * padding.ceil, + message, + '-' * padding.floor + ].join + end + + [ + pad.call("BEGIN #{product_name} LICENSE", 60), + data.strip, + pad.call("END #{product_name} LICENSE", 60) + ].join("\n") + end + + def remove_boundary(data) + after_boundary = data.split(BOUNDARY_START).last + in_boundary = after_boundary.split(BOUNDARY_END).first + + in_boundary + end + end + end + end +end + +module Gitlab + class License + class Error < StandardError; end + class ImportError < Error; end + class ValidationError < Error; end + + class << self + attr_reader :encryption_key + @encryption_key = nil + + def encryption_key=(key) + raise ArgumentError, 'No RSA encryption key provided.' if key && !key.is_a?(OpenSSL::PKey::RSA) + + @encryption_key = key + @encryptor = nil + end + + def encryptor + @encryptor ||= Encryptor.new(encryption_key) + end + + def import(data) + raise ImportError, 'No license data.' if data.nil? + + data = Boundary.remove_boundary(data) + + begin + license_json = encryptor.decrypt(data) + rescue Encryptor::Error + raise ImportError, 'License data could not be decrypted.' + end + + begin + attributes = JSON.parse(license_json) + rescue JSON::ParseError + raise ImportError, 'License data is invalid JSON.' + end + + new(attributes) + end + end + + attr_reader :version + attr_accessor :licensee, :starts_at, :expires_at, :notify_admins_at, + :notify_users_at, :block_changes_at, :last_synced_at, :next_sync_at, + :activated_at, :restrictions, :cloud_licensing_enabled, + :offline_cloud_licensing_enabled, :auto_renew_enabled, :seat_reconciliation_enabled, + :operational_metrics_enabled, :generated_from_customers_dot + + alias_method :issued_at, :starts_at + alias_method :issued_at=, :starts_at= + + def initialize(attributes = {}) + load_attributes(attributes) + end + + def valid? + if !licensee || !licensee.is_a?(Hash) || licensee.empty? + false + elsif !starts_at || !starts_at.is_a?(Date) + false + elsif expires_at && !expires_at.is_a?(Date) + false + elsif notify_admins_at && !notify_admins_at.is_a?(Date) + false + elsif notify_users_at && !notify_users_at.is_a?(Date) + false + elsif block_changes_at && !block_changes_at.is_a?(Date) + false + elsif last_synced_at && !last_synced_at.is_a?(DateTime) + false + elsif next_sync_at && !next_sync_at.is_a?(DateTime) + false + elsif activated_at && !activated_at.is_a?(DateTime) + false + elsif restrictions && !restrictions.is_a?(Hash) + false + elsif !cloud_licensing? && offline_cloud_licensing? + false + else + true + end + end + + def validate! + raise ValidationError, 'License is invalid' unless valid? + end + + def will_expire? + expires_at + end + + def will_notify_admins? + notify_admins_at + end + + def will_notify_users? + notify_users_at + end + + def will_block_changes? + block_changes_at + end + + def will_sync? + next_sync_at + end + + def activated? + activated_at + end + + def expired? + will_expire? && Date.today >= expires_at + end + + def notify_admins? + will_notify_admins? && Date.today >= notify_admins_at + end + + def notify_users? + will_notify_users? && Date.today >= notify_users_at + end + + def block_changes? + will_block_changes? && Date.today >= block_changes_at + end + + def cloud_licensing? + cloud_licensing_enabled == true + end + + def offline_cloud_licensing? + offline_cloud_licensing_enabled == true + end + + def auto_renew? + auto_renew_enabled == true + end + + def seat_reconciliation? + seat_reconciliation_enabled == true + end + + def operational_metrics? + operational_metrics_enabled == true + end + + def generated_from_customers_dot? + generated_from_customers_dot == true + end + + def restricted?(key = nil) + if key + restricted? && restrictions.has_key?(key) + else + restrictions && restrictions.length >= 1 + end + end + + def attributes + hash = {} + + hash['version'] = version + hash['licensee'] = licensee + + hash['issued_at'] = starts_at + hash['expires_at'] = expires_at if will_expire? + + hash['notify_admins_at'] = notify_admins_at if will_notify_admins? + hash['notify_users_at'] = notify_users_at if will_notify_users? + hash['block_changes_at'] = block_changes_at if will_block_changes? + + hash['next_sync_at'] = next_sync_at if will_sync? + hash['last_synced_at'] = last_synced_at if will_sync? + hash['activated_at'] = activated_at if activated? + + hash['cloud_licensing_enabled'] = cloud_licensing? + hash['offline_cloud_licensing_enabled'] = offline_cloud_licensing? + hash['auto_renew_enabled'] = auto_renew? + hash['seat_reconciliation_enabled'] = seat_reconciliation? + hash['operational_metrics_enabled'] = operational_metrics? + + hash['generated_from_customers_dot'] = generated_from_customers_dot? + + hash['restrictions'] = restrictions if restricted? + + hash + end + + def to_json(*_args) + JSON.dump(attributes) + end + + def export(boundary: nil) + validate! + + puts to_json + + data = self.class.encryptor.encrypt(to_json) + + data = Boundary.add_boundary(data, boundary) if boundary + + data + end + + private + + def load_attributes(attributes) + attributes = attributes.transform_keys(&:to_s) + + version = attributes['version'] || 1 + raise ArgumentError, 'Version is too new' unless version && version == 1 + + @version = version + + @licensee = attributes['licensee'] + + %w[issued_at expires_at notify_admins_at notify_users_at block_changes_at].each do |attr_name| + set_date_attribute(attr_name, attributes[attr_name]) + end + + %w[last_synced_at next_sync_at activated_at].each do |attr_name| + set_datetime_attribute(attr_name, attributes[attr_name]) + end + + %w[ + cloud_licensing_enabled + offline_cloud_licensing_enabled + auto_renew_enabled + seat_reconciliation_enabled + operational_metrics_enabled + generated_from_customers_dot + ].each do |attr_name| + public_send("#{attr_name}=", attributes[attr_name] == true) + end + + restrictions = attributes['restrictions'] + if restrictions&.is_a?(Hash) + restrictions = restrictions.transform_keys(&:to_sym) + @restrictions = restrictions + end + end + + def set_date_attribute(attr_name, value, date_class = Date) + value = date_class.parse(value) rescue nil if value.is_a?(String) + + return unless value + + public_send("#{attr_name}=", value) + end + + def set_datetime_attribute(attr_name, value) + set_date_attribute(attr_name, value, DateTime) + end + end +end + +# MARK: GENERATOR + +if !File.file?("license_key") || !File.file?("license_key.pub") + puts "License key not found" + puts "Generate a RSA key pair using generate_keys.rb" + exit +end + +public_key = OpenSSL::PKey::RSA.new File.read("license_key.pub") +private_key = OpenSSL::PKey::RSA.new File.read("license_key") + +Gitlab::License.encryption_key = private_key + +license = Gitlab::License.new + +license.licensee = { + "Name" => "GitLab Inc.", + "Company" => "GitLab Inc.", + "Email" => "support@gitlab.com" +} + +license.starts_at = Date.new(2000, 1, 1) +license.restrictions = { + plan: 'ultimate', + active_user_count: 100000000, +} + +data = license.export +File.open("result.gitlab-license", "w") { |f| f.write(data) } diff --git a/license_key b/license_key new file mode 100644 index 000000000..665e32a --- /dev/null +++ b/license_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAud4kAhXh7dQg9qlhqbwUd1wrknf06vrT2Wc72lhag5jMtWSs +HWEC817zNOTNkev3q6RYXUO0cm+fcZsO4g3Iy4ZbM9s5kXY1jDaM4Mc6JCehO7o1 +1f4mY6FY6D1RHpk7GCKkS7ApbIjYkNt7TRYVNUeMoY82g14NWjehWroOi23FOn4U +XPX20mBCRjK0RFLQ26j0rWxHpD3yz0cbY8qx3b9v/Jazlk282IakorxaHVTYN5ap +lspif3M1Ku36mRVXUVkoYRJl8vjUgicut6vcqzrTk14l3quyPKri4Tnps/ZLvWGp +d53UvEEevZxJVKVxdtgH0O2UCRVRXSuiG74ZCwIDAQABAoIBAEDlIKlhvoptQD0f +EqxSsMqj8cqn+2l3vjPv6WPo6WF9HixPRBDV6FPU2RGkuWmze7wAG6Ikm4JBGuht +fRrMOUlmVb2bU1RIc5XLDhEFPnWVKKRT9awLmpe6o/IiRopqccmRfs+2aCAu/35E +Q568kRcTLjTSbfQcCIlxVvL4d0+SoYbYnuUWZ5oM20FC/HcLjWRlEW8UAQ0guKgw +MRw4Yw1vOOZqzsrQThWcam7DgyhOs3oCAbV3j4ZaoZpmDgLRt1Kvx2ZGUOoqh/ly +rONuah1ESGrLmNy4NLNlXI6IArisoZQj2uosC7nbIlmLkuRhFNLPjx/nw66FGw1A +8pP56EECgYEA5JLKSJy9RH6nS52h0UmnrhmkwJloLm/s4bVgE6xD8V2O9Lk+nUYz +8MEpGUgpN1WenjTz7EruANZtIP9qx7mPptEk34D+JBEprS/gMaaKb9hKJ46kNTqp +zrN7CUsk5nHGaKCChJA4JCUm2LCNna3gB2Bf3bcliXvJQb9Sa7Kk61ECgYEA0CuJ +wUKLkX8xfJNUKYHFx8GysNfgYqxjFOUWHcwM7LHWgVS1+/sgym+4qFbn35aIHN6G +87hHddgT+XyVVz8GTZ5DykA1SvvLJZOKM4cxP/EkkI0sSJLPmETC7Py2gZs0Z3qF +KOipcchosrMbpLzmprRkfzlGwl1nhU7/JCRD75sCgYA+OHc4LPKYoqGHw/E4t4Qd +sH1YsGnbujwRdP4iXNJh8cXoeETDK0kYUHyPlUUi+vuitWdw+zSupbAvO1gl5i1k +i6ot7T9BMirWKiItYdhtecM14W5xzvZKfjEP5pS05mPMN2VQELI3pKVedzEVqy9A +0stF34UoV7oBW8Nj7c1XAQKBgDyCi1Zb64nteQsHIE24ZS89hJ2XAqhsB5kJRjZ/ +G7qprvqFDykhxFRTyU9Vg60gaoxJutyZUlxU5Ol+Z0KnFUP2nynpJBSZwGE509BK +mexGQiSqhJbL5gAS7L5KbxqZbNAvcwmDJ83lPVnEamKmbj1C7nt0wLa6w96iKdPt +nrnFAoGBAKTcZDk/YecmnvRYNB5su5bQLjQZZhdGECvEvVBMyqm87pFUwjuVIkG7 +pYs2xLsC58kieI7fbWQ+ZG8ZTIs+bdOkJpGFYOekZAYeTJPep/RIVpskcpfPhEit +AG0SbqXcVZTzY+kRTSttBijUbvYGsHccUSJnvJY65FQC6OZJCDkz +-----END RSA PRIVATE KEY----- diff --git a/license_key.pub b/license_key.pub new file mode 100644 index 000000000..fcd4b9d --- /dev/null +++ b/license_key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAud4kAhXh7dQg9qlhqbwU +d1wrknf06vrT2Wc72lhag5jMtWSsHWEC817zNOTNkev3q6RYXUO0cm+fcZsO4g3I +y4ZbM9s5kXY1jDaM4Mc6JCehO7o11f4mY6FY6D1RHpk7GCKkS7ApbIjYkNt7TRYV +NUeMoY82g14NWjehWroOi23FOn4UXPX20mBCRjK0RFLQ26j0rWxHpD3yz0cbY8qx +3b9v/Jazlk282IakorxaHVTYN5aplspif3M1Ku36mRVXUVkoYRJl8vjUgicut6vc +qzrTk14l3quyPKri4Tnps/ZLvWGpd53UvEEevZxJVKVxdtgH0O2UCRVRXSuiG74Z +CwIDAQAB +-----END PUBLIC KEY----- diff --git a/make.sh b/make.sh new file mode 100755 index 000000000..e6aa410 --- /dev/null +++ b/make.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +ruby ./generate_keys.rb +ruby ./generate_licenses.rb + +echo "done" \ No newline at end of file diff --git a/result.gitlab-license b/result.gitlab-license new file mode 100644 index 000000000..b532deb --- /dev/null +++ b/result.gitlab-license @@ -0,0 +1,22 @@ +eyJkYXRhIjoieThyOWZHMjQyMGlMcEFnaS9nOFYxZmNobitoakxWSmszYTRU +czhTdTZ0VG80akFLdXhNM1NQbGJMNi92XG4yRFNQcXJBQWltRE1vVnNmSkJu +ZG9uUVA5WHByRlJ1NlhHQndBK3p3eDB2b01hNURLNlVaS3hYUDN5QnFcbnda +VzdLanpUK0lYNU5nNmlGSktLWFJVdy9nNnRYeHFmdEVtc2laK0RlR3FNbzl3 +M0VBT0t4cHgyN2Z0clxuQ2VHckZqQTNaVGpVRkdkMWNwTlE5dHJjR21oQnhI +TXdmQlp5OUFVZFNWR2RkeC9HMU85N24xNC9sRG56XG5lVTJHUmRvcUtUbmtv +bnJkQzlFOTBuY21ERkFlaTQ1dGlrM2V4enFlMjBEN3hFbFQ5bm80N3lXT2VR +QU9cbitLMTBwZlMzWnoxNTBNSk9XZTBxUVdkeE8wQ0FwNVI3NFJmUjQ2Zlg0 +Tkd4WDlCSGhuK1U4TWZ3aDRhWlxuMU9ZT2FxemFwUDFiQmZNNnNHenV6bHJT +SnN0Um8yUHFEYVp6WlJjZEtSQnU0elRPV1FxSzBzVmlzWFhvXG55TDJxM3l1 +cmFWaFA1UW5DK2ljL0VDcGJ0M2w3dWtNR095UCtDc0hPRWsveE5jekdXQWNo +V2xmYzVaNTZcbld6UnFpcnkvR1dWR1ZJNXpMMUt0ekFVS2FlMkZtYXFxSFoy +WjlKK3JSdGxXMkR4T3NCOWpidz09XG4iLCJrZXkiOiJZby9MK0xBQy9tRWZz +MTVnTVI4TTNLQ29zdDQzTkVJY01zQXlKY1JVeGs4aG1mV2xLZmpXWkhBaG9D +TUlcbkZzanloalpWeUsvMkVHOFNML05uQmQzckErYjl5T3RlbEpFOTMzQko1 +aFZWcTdrZFRBNE5CTTlGYzArWlxueFJtTDUzcXNiQlA1MUtDL2t4K2t1ZDRJ +ZXNFalFiRk5NQVpyWGE5Rkd6VmVySUR2U2phQzZPc3BwQ0lVXG5PTjR0NEhP +eTQ5dUszbUdPRDlvMGRZTUNCd25qMW50RzZhUlNhRjBhZXlQbS9rZy9ZcHhv +QTExZ2k3UWtcbmtHbCtuSzBnZEtkMGNnemk1TzlUcDBCUHVEWTVZd2wwcFlW +WGJSNU4yWE90WlBnWFJkSmJqMjVMK1F5VlxuNmZGcnBHZmxzdllaWjB3OEFH +TnBlQU5WS3l4bytBVVVEcTlMNlgzdU5BPT1cbiIsIml2IjoielJPZGwxbTcr +b1NKVmptYmFDaWUwQT09XG4ifQ==