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