This commit implements a MinIO specific request encryption/decryption logic. It is not documented, nor listed anywhere except the oficially supported SDKs (Python, GO) use these. Therefore, the logic is reverse engineered from the existing SDKs (https://github.com/minio/minio-py, https://github.com/minio/madmin-go/tree/main) Figuring out request body encrypt/decrypt operation by reading other source code without proper documentation was painful. So, here is my understanding on how they perform encryption. The decryption is simply doing the same thing but decrypting instead of encrypting. 1. First 32 bytes of a request body is SALT. That is a set of random bytes to use together with the secret key to encrypt the data. 2. Next 8 bytes of a request body is NONCE. These are again random bytes used to encrypt the request body and extra bytes from NONCE because NONCE should be 12 bytes in Argon but we use only 8 for MinIO. 3. Next bytes until the last 16th byte is the request body in encrypted form using AesGcmCipherProvider 4. Last 16 bytes are what is called as hmac_tag that is generated by the full length of nonce (12 bytes) in encrypted form. This commit comes with 2 API implementation that makes use of both encrypt and decrypt functionality; 1. admin_list_users: lists users and some of their properties. The response is sent in encrypted form from the server, therefore, we decrypt it using the credentials and the algorith explained above. 2. admin_add_user: adds a new user to the system with access_key and secret_key. Since keys must not leak outside of the system, the request body is encrypted using the client user secret_key and the algorithm explained above.
31 lines
1.2 KiB
Ruby
31 lines
1.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "base64"
|
|
RSpec.describe Minio::Crypto do
|
|
describe "encrypt" do
|
|
it "can encrypt a payload" do
|
|
expect(SecureRandom).to receive(:random_bytes).with(8).and_return("noncenon")
|
|
expect(SecureRandom).to receive(:random_bytes).with(32).and_return("saltsaltsaltsaltsaltsaltsaltsalt")
|
|
payload = "test"
|
|
password = "password"
|
|
expect(Base64.encode64(described_class.new.encrypt(payload, password))).to eq("c2FsdHNhbHRzYWx0c2FsdHNhbHRzYWx0c2FsdHNhbHQAbm9uY2Vub24Q8NnE\ng9RvKzKmwFclyEJrPFFKJA==\n")
|
|
end
|
|
end
|
|
|
|
describe "decrypt" do
|
|
it "can decrypt a payload" do
|
|
payload = "c2FsdHNhbHRzYWx0c2FsdHNhbHRzYWx0c2FsdHNhbHQAbm9uY2Vub24Q8NnE\ng9RvKzKmwFclyEJrPFFKJA==\n"
|
|
password = "password"
|
|
expect(described_class.new.decrypt(Base64.decode64(payload), password)).to eq("test")
|
|
end
|
|
|
|
it "fails if cipher is not known" do
|
|
payload = "111111111111111111111111111111111111111111111111111111111111\ng9RvKzKmwFclyEJrPFFKJA==\n"
|
|
password = "password"
|
|
expect {
|
|
described_class.new.decrypt(Base64.decode64(payload), password)
|
|
}.to raise_error RuntimeError, "Unsupported cipher ID: 117"
|
|
end
|
|
end
|
|
end
|