@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
66import Debug from 'debug' ;
77import type { Config } from '../config' ;
88import { GpgErrorCode , identifyError } from './gpgError' ;
9+ import { isExecutableSync } from 'is-executable' ;
910
1011const log = Debug ( 'cid::engine::crypto::gpg' ) ;
1112
@@ -15,8 +16,7 @@ export type GpgOptions = {
1516 binaryPathOverride ?: string ;
1617 pinentryMode ?: Config . CryptoPinentryMode ;
1718 timeoutMs ?: number ;
18- verifyRecipientKey ?: boolean ;
19- verifySignerKey ?: boolean ;
19+ verifyKeys ?: boolean ;
2020}
2121
2222
@@ -54,7 +54,7 @@ export class GpgWrapper {
5454 // For headless usage or usage on Linux/MacOS, users should ensure GPG is set on PATH or provide
5555 // path explicitly.
5656 if ( isWindows ( ) ) {
57- const candidates = resolveWindowsGpgBinCandidates ( ) ;
57+ const candidates = this . resolveWindowsGpgBinCandidates ( ) ;
5858 for ( const c of candidates ) if ( existsSync ( c ) ) {
5959 log ( `[INFO] Found GPG binary at common location: ${ c } ` ) ;
6060 return c ;
@@ -80,6 +80,21 @@ export class GpgWrapper {
8080 return 'gpg' ;
8181 }
8282
83+ private resolveWindowsGpgBinCandidates ( ) : string [ ] {
84+ const localAppData = process . env . LOCALAPPDATA ;
85+ const programFiles = process . env [ 'ProgramFiles' ] ;
86+ const programFilesX86 = process . env [ 'ProgramFiles(x86)' ] ;
87+
88+ // Order matters
89+ const opts : string [ ] = [ ] ;
90+ if ( localAppData ) opts . push ( join ( localAppData , 'Programs' , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
91+ if ( programFiles ) opts . push ( join ( programFiles , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
92+ if ( programFilesX86 ) opts . push ( join ( programFilesX86 , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
93+ if ( programFiles ) opts . push ( join ( programFiles , 'Gpg4win' , 'bin' , 'gpg.exe' ) ) ;
94+ if ( programFilesX86 ) opts . push ( join ( programFilesX86 , 'Gpg4win' , 'bin' , 'gpg.exe' ) ) ;
95+ return opts ;
96+ }
97+
8398 private keyExists ( key : string , keyType : "RECIPIENT" | "SIGNER" ) : boolean {
8499 const args = keyType === "RECIPIENT" ? [ '--list-keys' , '--with-colons' , key ] : [ '--list-secret-keys' , '--with-colons' , key ] ;
85100 const r = spawnSync ( this . binPath , args , { encoding : "utf-8" } ) ;
@@ -88,12 +103,17 @@ export class GpgWrapper {
88103
89104 public async encryptFile ( { inputPath, outputPath, recipient, signer, signerPassphrase } : EncryptFileInput ) : Promise < EncryptFileResult > {
90105 // TODO: move this into the constructor?
106+
107+ // TODO: find an alternative for checking existence/accessibility of GPG binary.
108+ // since access() on some platforms (Windows) may return false negatives due to ACLs.
109+ // For now, swapping in 3rd party library (isExecutable) to get it working, but perhaps
110+ // it is better to just attempt to run GPG and handle the errors?
91111 try {
92112 log ( `[DEBUG] Checking GPG binary at path: '${ this . binPath } '` ) ;
93- await access ( this . binPath , constants . X_OK ) ;
113+ isExecutableSync ( this . binPath ) ;
94114 log ( `[DEBUG] GPG binary is executable.` ) ;
95115 }
96- catch {
116+ catch ( err ) {
97117 log ( `[ERROR] GPG binary not found or not executable at path: '${ this . binPath } '` ) ;
98118 return { success : false , error : `GPG binary not found or not executable at path: '${ this . binPath } '` , code : GpgErrorCode . GPG_NOT_FOUND }
99119 }
@@ -122,7 +142,7 @@ export class GpgWrapper {
122142 }
123143
124144 // check recipient key is in keyring and valid
125- if ( this . options . verifyRecipientKey ) {
145+ if ( this . options . verifyKeys ) {
126146 log ( `[DEBUG] Verifying recipient key exists in keyring: '${ recipient } '` ) ;
127147 const okay = this . keyExists ( recipient , "RECIPIENT" ) ;
128148 if ( ! okay ) {
@@ -134,7 +154,7 @@ export class GpgWrapper {
134154 }
135155
136156 // check signer key is in the keyring and valid
137- if ( signer && this . options . verifySignerKey ) {
157+ if ( signer && signer . length > 0 && this . options . verifyKeys ) {
138158 log ( `[DEBUG] Verifying signer secret key exists in keyring: '${ signer } '` ) ;
139159 const okay = this . keyExists ( signer , "SIGNER" ) ;
140160 if ( ! okay ) {
@@ -154,7 +174,7 @@ export class GpgWrapper {
154174
155175 args . push ( '--output' , outputPath ) ;
156176
157- if ( signer ) {
177+ if ( signer && signer . length > 0 ) {
158178 args . push ( '--sign' , '--local-user' , signer ) ;
159179 if ( this . options . pinentryMode === "loopback" && ! signerPassphrase ) {
160180 log ( `[WARN] pinentry-mode=loopback is set but no passphrase has been provided; signing may fail if the key is protected.` ) ;
@@ -210,20 +230,5 @@ export class GpgWrapper {
210230
211231}
212232
213- function resolveWindowsGpgBinCandidates ( ) : string [ ] {
214- const localAppData = process . env . LOCALAPPDATA ;
215- const programFiles = process . env [ 'ProgramFiles' ] ;
216- const programFilesX86 = process . env [ 'ProgramFiles(x86)' ] ;
217-
218- // Order matters
219- const opts : string [ ] = [ ] ;
220- if ( localAppData ) opts . push ( join ( localAppData , 'Programs' , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
221- if ( programFiles ) opts . push ( join ( programFiles , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
222- if ( programFilesX86 ) opts . push ( join ( programFilesX86 , 'GnuPG' , 'bin' , 'gpg.exe' ) ) ;
223- if ( programFiles ) opts . push ( join ( programFiles , 'Gpg4win' , 'bin' , 'gpg.exe' ) ) ;
224- if ( programFilesX86 ) opts . push ( join ( programFilesX86 , 'Gpg4win' , 'bin' , 'gpg.exe' ) ) ;
225- return opts ;
226- }
227-
228233
229234export const isWindows = ( ) => process . platform === 'win32' ;
0 commit comments