在 iOS 开发中,证书管理一直是一个重要且复杂的话题。随着 fastlane 的更新,我们处理证书信息的方式也在不断演进。今天,我想分享我在使用 fastlane 读取和加密 iOS 证书信息时的经历,以及如何应对 fastlane 更新带来的变化。
Table of contents
Open Table of contents
初始方法:直接读取证书文件
最初,我创建了一个自定义的 fastlane Action 来直接读取 .cer 文件,这里使用的是 OpenSSL 进行解密:
class ReadSpecificFileAction < Action
def self.decrypt_specific_file(path: nil, password: nil, hash_algorithm: "MD5")
stored_data = Base64.decode64(File.read(path))
salt = stored_data[8..15]
data_to_decrypt = stored_data[16..-1]
decipher = ::OpenSSL::Cipher.new('AES-256-CBC')
decipher.decrypt
decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)
decrypted_data = decipher.update(data_to_decrypt) + decipher.final
File.binwrite(path, decrypted_data)
rescue => error
fallback_hash_algorithm = "SHA256"
if hash_algorithm != fallback_hash_algorithm
decrypt_specific_file(path: path, password: password, hash_algorithm: fallback_hash_algorithm)
else
UI.error(error.to_s)
UI.crash!("Error decrypting '#{path}'")
end
end
end
这种方法简单直接,但随着 fastlane 的更新,fastlane 应该是修改了加密解密的逻辑,上面的方案已经无法使用,我们需要寻找新的实现方案。
寻找新的实现方案
Step 1: 查阅 fastlane match 插件的官方文档
在阅读 match 文档的过程中我看到一个手动解密的信息,原文如下:
Manual Decrypt
If you want to manually decrypt or encrypt a file, you can use the companion script
match_file:match_file encrypt "<fileYouWantToEncryptPath>" ["<encryptedFilePath>"] match_file decrypt "<fileYouWantToDecryptPath>" ["<decryptedFilePath>"]The password will be asked interactively.
Note: You may need to swap double quotes
"for single quotes'if your match password contains an exclamation mark!.
Step 2: 查看 match_file 命令的源码实现
既然我们知道了这里是使用 match_file 命令进行手动解密的,那么我们可以去 github 看一下他是如何实现的。match_file
begin
Match::Encryption::MatchFileEncryption.new.send(method_name, file_path: input_file, password: password, output_path: output_file)
rescue => e
puts("ERROR #{method_name}ing. [#{e}]. Check your password")
usage
end
这是脚本的核心部分,使用 Match::Encryption::MatchFileEncryption 类来执行加密或解密操作。如果发生错误(例如密码错误),会捕获异常并显示错误信息。
我们来看一下这个方法的参数都是什么:
method_name
- 这是第一个参数,它决定了要执行的操作。
- 值可以是
encrypt或decrypt。 - 这个参数使用 send 方法动态调用相应的方法(加密或解密)。
file_path: input_file
- 这是一个命名参数,指定要处理的 iOS 证书文件的路径。
input_file是之前从命令行参数中获取的值。
password: password
- 这个命名参数提供用于加密或解密的密码。
output_path: output_file
- 这个命名参数指定处理后文件的输出路径。
新方法:基于 match_file 的实现
我创建了一个新的 Action,名为 MatchFileEncryptionAction。
class MatchFileEncryptionAction < Action
def self.run(params)
method_name = params[:method]
input_file = params[:input_file]
output_file = params[:output_file] || input_file
password = params[:password]
begin
Match::Encryption::MatchFileEncryption.new.send(method_name, file_path: input_file, password: password, output_path: output_file)
rescue => e
UI.user_error!("ERROR #{method_name}ing. [#{e}]. Check your password")
end
end
end
可以看到,这个方法只是做了加密解密相关的事情,没有对证书信息进行读取,我将对证书的解析封装到了另一个 ReadCertificateInfoAction 中。
class ReadCertificateInfoAction < Action
def self.run(params)
cert_path = params[:cert_path]
UI.user_error!("Certificate file not found at path: #{cert_path}") unless File.exist?(cert_path)
begin
cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
subject = parse_subject(cert.subject)
info = {
subject: cert.subject.to_s,
issuer: cert.issuer.to_s,
serial: cert.serial.to_s,
not_before: cert.not_before,
not_after: cert.not_after,
public_key: cert.public_key.to_pem,
team_id: extract_team_id(subject),
description: extract_description(subject)
}
# Extract Subject Alternative Names if present
ext = cert.extensions.find { |e| e.oid == 'subjectAltName' }
info[:subject_alt_names] = ext.value if ext
return info
rescue OpenSSL::X509::CertificateError => e
UI.user_error!("Failed to read certificate: #{e.message}")
rescue => e
UI.user_error!("An error occurred: #{e.message}")
end
end
def self.parse_subject(subject)
subject.to_a.each_with_object({}) do |field, hash|
hash[field[0]] = field[1]
end
end
def self.extract_team_id(subject)
subject['OU'] || "Not found"
end
def self.extract_description(subject)
cn = subject['CN'] || ""
o = subject['O'] || ""
"#{cn}#{o.empty? ? '' : " (#{o})"}"
end
end
至此,我们已经能够在新版 fastlane 中进行对证书的解密和对证书的解析。
最后我们来看一下,iOS 证书里面我解析出了哪些信息
subject: 证书的主题,通常包含证书持有者的信息。issuer: 证书的颁发者,即签发这个证书的机构(通常是Apple)。serial: 证书的序列号,是一个唯一标识符。not_before: 证书的生效日期,在这个日期之前证书是无效的。not_after: 证书的过期日期,在这个日期之后证书将失效。public_key: 证书中包含的公钥,用于验证与之配对的私钥签名的内容。team_id: 开发者团队ID,这是从证书主题中提取出来的。每个Apple开发者账号都有一个唯一的团队ID。description: 证书的描述信息,也是从证书主题中提取的。