Introduction

I’ve been exploring ways to manage localization resource files for a while. Previously, our team tried Weblate, but we couldn’t fully adopt it mainly because of its high learning curve and unfriendly interaction for non-technical translators!

Recently, while browsing GitHub, I accidentally discovered an open source project called Texterify with only three-digit stars. The official website provides SaaS services, and after trying it out, I found it quite good in various aspects - it really resonated with me!

Overview

Texterify’s backend is developed using the Ruby on Rails framework and supports the following language file types:

  • iOS: .strings files
  • JSON: .json files
  • JSON POEditor: .json files
  • JSON FormatJS: .json files
  • Chrome JSON: .json files
  • go-i18n: .toml files
  • TOML: .toml files
  • Java: .properties files
  • gettext: .po files
  • Django: .po files
  • Flutter: .arb files
  • XLIFF: .xlf, .xliff files
  • Rails: .yml, .yaml files
  • YAML: .yml, .yaml files

Although it’s open source, the Self-Hosted version still requires purchasing a license, otherwise many features won’t be available. Its license authentication uses the Gitlab-License library, so the authorization issue can be solved technically.

Deployment

Following the official documentation, I used Docker Compose for a demo deployment, which went quite smoothly. However, if you want to customize some configurations, there are still some areas for improvement in the project. I’ve already submitted Issues and PRs on GitHub.

# Clone the docker-compose configuration.
git clone https://github.com/texterify/texterify-docker-compose-setup.git
cd texterify-docker-compose-setup

# Generate a secret key for the app.
# Make sure to keep this private.
echo SECRET_KEY_BASE=`openssl rand -hex 64` > secrets.env

# Open the .env file and replace "example.com" with your host (if you
# are trying to run Texterify locally just use "localhost" as host).
# Also make sure to check out the other configuration options (see below).

# Start the service.
docker volume create --name=texterify-database
docker volume create --name=texterify-assets
docker compose up --always-recreate-deps

# After everything has started create the database in another terminal.
docker compose exec app bin/rails db:create db:migrate db:seed

The issues I encountered are as follows:

  • The first issue is that the sidekiq service cannot get the REDIS_URL environment variable. I submitted an Issue.
  • The second issue is that the FORM in the SMTP configuration cannot be highly customized. I submitted a PR.

Pricing Model

After running the demo locally, I found that many features require upgrading the plan, similar to the Gitlab CE model. For specific plans, you can refer to the official website.

So I analyzed Texterify’s source code and found that the license authorization mainly uses the Gitlab::License package.

After a quick look, I discovered that this authorization mode uses asymmetric encryption to encrypt license information, and then uses a public key for decryption to obtain the license information!

Cracking

Knowing the authorization principle of the license, the next step became easier. I realized that I just needed to replace the license_key.pub file in the Docker Image to validate a self-generated license.

Prepare a script file license.rb for generating the license:

require "openssl"
require "gitlab/license"

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

private_key = OpenSSL::PKey::RSA.new File.read("license_key")
Gitlab::License.encryption_key = private_key

license = Gitlab::License.new
license.licensee = {
    "name" => "George",
    "email" => "[email protected]",
    "company" => "Betterde Inc.",
}
license.starts_at = Date.new(2020, 1, 1) # Start date
license.expires_at = Date.new(2050, 1, 1) # End date
license.notify_admins_at = Date.new(2049, 12, 1)
license.notify_users_at = Date.new(2049, 12, 1)
license.block_changes_at = Date.new(2050, 1, 1)
license.restrictions = {
    plan: "business", active_users_count: 3000
}

puts "License:"
puts license

data = license.export
puts "Exported license:"
puts data
File.open("texterify.texterify-license", "w") { |f| f.write(data) }

public_key = OpenSSL::PKey::RSA.new File.read("license_key.pub")
Gitlab::License.encryption_key = public_key

data = File.read("texterify.texterify-license")
$license = Gitlab::License.import(data)

puts "Imported license:"
puts $license

unless $license
raise "The license is invalid."
end

if $license.restricted?(:active_user_count)
active_user_count = 10000
if active_user_count > $license.restrictions[:active_user_count]
    raise "The active user count exceeds the allowed amount!"
end
end

if $license.notify_admins?
puts "The license is due to expire on #{$license.expires_at}."
end

if $license.notify_users?
puts "The license is due to expire on #{$license.expires_at}."
end

module Gitlab
class GitAccess
    def check(cmd, changes = nil)
    if $license.block_changes?
        return build_status_object(false, "License expired")
    end
    end
end
end

puts "This instance of Texterify Enterprise Edition is licensed to:"
$license.licensee.each do |key, value|
puts "#{key}: #{value}"
end

if $license.expired?
puts "The license expired on #{$license.expires_at}"
elsif $license.will_expire?
puts "The license will expire on #{$license.expires_at}"
else
puts "The license will never expire."
end

The main parts that need to be replaced are the information in licensee, the expiration time, and most importantly, license.restrictions. Since we’re going all out, let’s set the plan to business, and active_users_count is used to limit the number of users!

Then execute the following command on macOS to generate the license:

gem install gitlab-license

ruby license.rb

ls -la
total 32
drwxr-xr-x   6 George  staff   192 Mar  9 15:36 .
drwx------@ 30 George  staff   960 Mar  9 15:36 ..
-rw-r--r--@  1 George  staff  2168 Mar  8 15:57 license.rb
-rw-r--r--   1 George  staff  1679 Mar  8 15:57 license_key
-rw-r--r--   1 George  staff   451 Mar  8 15:57 license_key.pub
-rw-r--r--   1 George  staff  1489 Mar  8 15:57 texterify.texterify-license

Then log in to Texterify’s admin dashboard and upload the license:

Console

Since I didn’t enter the active_users_count information, it’s not displayed here!

I hope this is helpful, Happy hacking…