github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/tests/system/lib/bitwarden/organization.rb (about)

     1  class Bitwarden
     2    class Organization
     3      attr_reader :id, :name, :key
     4  
     5      def self.doctype
     6        "com.bitwarden.organizations"
     7      end
     8  
     9      def initialize(opts)
    10        @id = opts[:id]
    11        @name = opts[:name] || Faker::TvShows::DrWho.character
    12        @key = opts[:key]
    13      end
    14  
    15      def self.create(inst, name)
    16        opts = {
    17          accept: "application/json",
    18          content_type: "application/json",
    19          authorization: "Bearer #{inst.token_for doctype}"
    20        }
    21        org_key = generate_key
    22        user_key = get_private_key(inst)
    23        encrypted_key = encrypt_key(user_key, org_key)
    24        encrypted_name = encrypt_name(org_key, name)
    25        data = {
    26          name: name,
    27          key: encrypted_key,
    28          collectionName: encrypted_name
    29        }
    30        body = JSON.generate data
    31        res = inst.client["/bitwarden/api/organizations"].post body, opts
    32        body = JSON.parse(res.body)
    33        Organization.new(id: body["Id"], name: body["Name"], key: org_key)
    34      end
    35  
    36      def self.delete(inst, org_id)
    37        body = JSON.generate({ masterPasswordHash: inst.hashed_passphrase })
    38        opts = {
    39          accept: "application/json",
    40          content_type: "application/json",
    41          authorization: "Bearer #{inst.token_for doctype}"
    42        }
    43        url = "http://#{inst.domain}/bitwarden/api/organizations/#{org_id}"
    44        RestClient::Request.execute method: :delete, url: url, payload: body, headers: opts
    45      end
    46  
    47      def self.generate_key
    48        Random.bytes 64
    49      end
    50  
    51      def self.encrypt_key(user_key, org_key)
    52        encoded_payload = Base64.strict_encode64 org_key
    53        result = `cozy-stack tools encrypt-with-rsa '#{user_key}' '#{encoded_payload}'`
    54        code = $?
    55        ap "Status code #{code} for encrypt-with-rsa" unless code.success?
    56        result.chomp
    57      end
    58  
    59      def self.get_private_key(inst)
    60        opts = {
    61          accept: "application/json",
    62          authorization: "Bearer #{inst.token_for 'com.bitwarden.profiles'}"
    63        }
    64        res = inst.client["/bitwarden/api/accounts/profile"].get opts
    65        body = JSON.parse(res.body)
    66        sym_key = decrypt_symmetric_key(inst, body["Key"])
    67        decrypted = decrypt_private_key(sym_key, body["PrivateKey"])
    68        Base64.strict_encode64 decrypted
    69      end
    70  
    71      def self.decrypt_symmetric_key(inst, encrypted)
    72        master_key = PBKDF2.new do |p|
    73          p.password = inst.passphrase
    74          p.salt = "me@" + inst.domain.split(':').first
    75          p.iterations = 650_000 # See pkg/crypto/pbkdf2.go
    76          p.hash_function = OpenSSL::Digest::SHA256
    77          p.key_length = 256 / 8
    78        end.bin_string
    79  
    80        iv, data = encrypted.sub("0.", "").split("|")
    81        iv = Base64.strict_decode64(iv)
    82        data = Base64.strict_decode64(data)
    83  
    84        cipher = OpenSSL::Cipher.new "AES-256-CBC"
    85        cipher.decrypt
    86        cipher.key = master_key
    87        cipher.iv = iv
    88        decrypted = cipher.update(data)
    89        decrypted << cipher.final
    90        decrypted
    91      end
    92  
    93      def self.decrypt_private_key(sym_key, encrypted)
    94        iv, data, mac = encrypted.sub("2.", "").split("|")
    95        iv = Base64.strict_decode64(iv)
    96        data = Base64.strict_decode64(data)
    97  
    98        cipher = OpenSSL::Cipher.new "AES-256-CBC"
    99        cipher.decrypt
   100        cipher.key = sym_key[0...32]
   101        cipher.iv = iv
   102        decrypted = cipher.update(data)
   103        decrypted << cipher.final
   104  
   105        computed_mac = OpenSSL::HMAC.digest("SHA256", sym_key[32...64], iv + data)
   106        encoded_mac = Base64.strict_encode64(computed_mac)
   107        raise "Invalid mac" if encoded_mac != mac
   108  
   109        decrypted
   110      end
   111  
   112      def self.encrypt_name(key, name)
   113        enc_key = key[0...32]
   114        mac_key = key[32...64]
   115        iv = Random.bytes 16
   116        cipher = OpenSSL::Cipher.new "AES-256-CBC"
   117        cipher.encrypt
   118        cipher.key = enc_key
   119        cipher.iv = iv
   120        data = cipher.update(name)
   121        data << cipher.final
   122        mac = OpenSSL::HMAC.digest("SHA256", mac_key, iv + data)
   123        "2.#{Base64.strict_encode64 iv}|#{Base64.strict_encode64 data}|#{Base64.strict_encode64 mac}"
   124      end
   125    end
   126  end