summaryrefslogtreecommitdiff
path: root/lib/gitlab/gpg.rb
blob: 413872d7e081bc8d223de4d85c01a56c90bca8f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
module Gitlab
  module Gpg
    extend self

    MUTEX = Mutex.new

    module CurrentKeyChain
      extend self

      def add(key)
        GPGME::Key.import(key)
      end

      def fingerprints_from_key(key)
        import = GPGME::Key.import(key)

        return [] if import.imported == 0

        import.imports.map(&:fingerprint)
      end
    end

    def fingerprints_from_key(key)
      using_tmp_keychain do
        CurrentKeyChain.fingerprints_from_key(key)
      end
    end

    def primary_keyids_from_key(key)
      using_tmp_keychain do
        fingerprints = CurrentKeyChain.fingerprints_from_key(key)

        GPGME::Key.find(:public, fingerprints).map { |raw_key| raw_key.primary_subkey.keyid }
      end
    end

    def subkeys_from_key(key)
      using_tmp_keychain do
        fingerprints = CurrentKeyChain.fingerprints_from_key(key)
        raw_keys     = GPGME::Key.find(:public, fingerprints)

        raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
          primary_subkey_id = raw_key.primary_subkey.keyid

          grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
            { keyid: s.keyid, fingerprint: s.fingerprint }
          end
        end
      end
    end

    def user_infos_from_key(key)
      using_tmp_keychain do
        fingerprints = CurrentKeyChain.fingerprints_from_key(key)

        GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
          raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } }
        end
      end
    end

    # Allows thread safe switching of temporary keychain files
    #
    # 1. The current thread may use nesting of temporary keychain
    # 2. Another thread needs to wait for the lock to be released
    def using_tmp_keychain(&block)
      if MUTEX.locked? && MUTEX.owned?
        optimistic_using_tmp_keychain(&block)
      else
        MUTEX.synchronize do
          optimistic_using_tmp_keychain(&block)
        end
      end
    end

    # 1. Returns the custom home directory if one has been set by calling
    #    `GPGME::Engine.home_dir=`
    # 2. Returns the default home directory otherwise
    def current_home_dir
      GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir')
    end

    private

    def optimistic_using_tmp_keychain
      previous_dir = current_home_dir
      tmp_dir = Dir.mktmpdir
      GPGME::Engine.home_dir = tmp_dir
      yield
    ensure
      # Ignore any errors when removing the tmp directory, as we may run into a
      # race condition:
      # The `gpg-agent` agent process may clean up some files as well while
      # `FileUtils.remove_entry` is iterating the directory and removing all
      # its contained files and directories recursively, which could raise an
      # error.
      FileUtils.remove_entry(tmp_dir, true)
      GPGME::Engine.home_dir = previous_dir
    end
  end
end