Skip to content

Commit d5f89a9

Browse files
authored
Prevent null exception, throw exception on secret key / password missing (#1898)
* Prevent NPE when a pgp key option is not passed * NIT change message texts, refactor url making * NIT remove hardcoded config key * Add checking for secret key in config in publish setup * NIT remove Scala CLI hardcoded reference * NIT make expected artifacts a set * Add test-only scala-cli config * NIT Make output cooler and not visible when signer=none passed * Exclude no password tests for native, add warning
1 parent 864e1a5 commit d5f89a9

File tree

5 files changed

+337
-116
lines changed

5 files changed

+337
-116
lines changed

modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala

Lines changed: 114 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import scala.cli.commands.shared.{
5353
}
5454
import scala.cli.commands.util.{BuildCommandHelpers, ScalaCliSttpBackend}
5555
import scala.cli.commands.{ScalaCommand, SpecificationLevel, WatchUtil}
56-
import scala.cli.config.{ConfigDb, Keys, PublishCredentials}
56+
import scala.cli.config.{ConfigDb, Keys, PasswordOption, PublishCredentials}
5757
import scala.cli.errors.{
5858
FailedToSignFileError,
5959
MalformedChecksumsError,
@@ -844,97 +844,123 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
844844
}
845845
}
846846

847-
val signerOpt = publishOptions.contextual(isCi).signer.orElse {
848-
if (repoParams.supportsSig)
849-
if (publishOptions.contextual(isCi).secretKey.isDefined) Some(PSigner.BouncyCastle)
850-
else if (publishOptions.contextual(isCi).gpgSignatureId.isDefined) Some(PSigner.Gpg)
851-
else if (repoParams.shouldSign) Some(PSigner.BouncyCastle)
852-
else None
853-
else None
847+
def getBouncyCastleSigner(
848+
secretKey: PasswordOption,
849+
secretKeyPasswordOpt: Option[PasswordOption]
850+
) = {
851+
val getLauncher: Supplier[Array[String]] = { () =>
852+
val buildOptions = builds.headOption.map(_.options)
853+
val archiveCache = buildOptions.map(_.archiveCache)
854+
.getOrElse(ArchiveCache())
855+
val fileCache = buildOptions.map(_.finalCache).getOrElse(FileCache())
856+
PgpExternalCommand.launcher(
857+
fileCache,
858+
archiveCache,
859+
logger,
860+
() => builds.head.options.javaHome().value.javaCommand,
861+
publishOptions.signingCli
862+
) match {
863+
case Left(e) => throw new Exception(e)
864+
case Right(binaryCommand) => binaryCommand.toArray
865+
}
866+
}
867+
868+
if (secretKeyPasswordOpt.isEmpty)
869+
logger.diagnostic("PGP signing with no password is not recommended since it's not stable")
870+
871+
if (forceSigningBinary)
872+
(new scala.cli.internal.BouncycastleSignerMakerSubst).get(
873+
secretKeyPasswordOpt.fold(null)(_.toCliSigning),
874+
secretKey.toCliSigning,
875+
getLauncher,
876+
logger
877+
)
878+
else
879+
(new BouncycastleSignerMaker).get(
880+
secretKeyPasswordOpt.fold(null)(_.toCliSigning),
881+
secretKey.toCliSigning,
882+
getLauncher,
883+
logger
884+
)
854885
}
855-
val signer: Signer = signerOpt match {
856-
case Some(PSigner.Gpg) =>
857-
publishOptions.contextual(isCi).gpgSignatureId match {
858-
case Some(gpgSignatureId) =>
859-
GpgSigner(
860-
GpgSigner.Key.Id(gpgSignatureId),
861-
extraOptions = publishOptions.contextual(isCi).gpgOptions
886+
887+
val signerKind: PSigner = publishOptions.contextual(isCi).signer.getOrElse {
888+
if (!repoParams.supportsSig)
889+
PSigner.Nop
890+
else if (publishOptions.contextual(isCi).gpgSignatureId.isDefined)
891+
PSigner.Gpg
892+
else if (repoParams.shouldSign)
893+
PSigner.BouncyCastle
894+
else
895+
PSigner.Nop
896+
}
897+
898+
def getSecretKeyPasswordOpt: Option[PasswordOption] = {
899+
val maybeSecretKeyPass = if (publishOptions.contextual(isCi).secretKeyPassword.isDefined)
900+
for {
901+
secretKeyPassConfigOpt <- publishOptions.contextual(isCi).secretKeyPassword
902+
secretKeyPass <- secretKeyPassConfigOpt.get(configDb()).toOption
903+
} yield secretKeyPass
904+
else
905+
for {
906+
secretKeyPassOpt <- configDb().get(Keys.pgpSecretKeyPassword).toOption
907+
secretKeyPass <- secretKeyPassOpt
908+
} yield secretKeyPass
909+
910+
maybeSecretKeyPass
911+
}
912+
913+
val signer: Either[BuildException, Signer] = signerKind match {
914+
// user specified --signer=gpg or --gpgKey=...
915+
case PSigner.Gpg =>
916+
publishOptions.contextual(isCi).gpgSignatureId.map { gpgSignatureId =>
917+
GpgSigner(
918+
GpgSigner.Key.Id(gpgSignatureId),
919+
extraOptions = publishOptions.contextual(isCi).gpgOptions
920+
)
921+
}.toRight(new MissingPublishOptionError(
922+
"ID of the GPG key",
923+
"--gpgKey",
924+
directiveName = ""
925+
))
926+
927+
// user specified --signer=bc or --secret-key=... or target repository requires signing
928+
// --secret-key-password is possibly specified (not mandatory)
929+
case PSigner.Nop | PSigner.BouncyCastle
930+
if publishOptions.contextual(isCi).secretKey.isDefined =>
931+
val secretKeyConfigOpt = publishOptions.contextual(isCi).secretKey.get
932+
for {
933+
secretKey <- secretKeyConfigOpt.get(configDb())
934+
} yield getBouncyCastleSigner(secretKey, getSecretKeyPasswordOpt)
935+
936+
// user specified --signer=bc or target repository requires signing
937+
// --secret-key-password is possibly specified (not mandatory)
938+
case PSigner.BouncyCastle =>
939+
val shouldSignMsg =
940+
if (repoParams.shouldSign) "signing is required for chosen repository" else ""
941+
for {
942+
secretKeyOpt <- configDb().get(Keys.pgpSecretKey).wrapConfigException
943+
secretKey <- secretKeyOpt.toRight(
944+
new MissingPublishOptionError(
945+
"secret key",
946+
"--secret-key",
947+
directiveName = "",
948+
configKeys = Seq(Keys.pgpSecretKey.fullName),
949+
extraMessage = shouldSignMsg
862950
)
863-
case None => NopSigner
864-
}
865-
case Some(PSigner.BouncyCastle) =>
866-
val getLauncher: Supplier[Array[String]] = { () =>
867-
val buildOptions = builds.headOption.map(_.options)
868-
val archiveCache = buildOptions.map(_.archiveCache)
869-
.getOrElse(ArchiveCache())
870-
val fileCache = buildOptions.map(_.finalCache).getOrElse(FileCache())
871-
PgpExternalCommand.launcher(
872-
fileCache,
873-
archiveCache,
874-
logger,
875-
() => builds.head.options.javaHome().value.javaCommand,
876-
publishOptions.signingCli
877-
) match {
878-
case Left(e) => throw new Exception(e)
879-
case Right(binaryCommand) => binaryCommand.toArray
880-
}
881-
}
882-
val secretKeyDetailsOpt = publishOptions.contextual(isCi).secretKey match {
883-
case Some(secretKey0) =>
884-
val secretKey = secretKey0.get(configDb()).orExit(logger).toCliSigning
885-
val secretKeyPassword = publishOptions
886-
.contextual(isCi)
887-
.secretKeyPassword
888-
.orNull
889-
.get(configDb())
890-
.orExit(logger)
891-
.toCliSigning
892-
Some((secretKey, secretKeyPassword))
893-
case None =>
894-
configDb().get(Keys.pgpSecretKey).wrapConfigException.orExit(logger) match {
895-
case Some(secretKey) =>
896-
val secretKeyPassword =
897-
configDb().get(Keys.pgpSecretKeyPassword).wrapConfigException
898-
.flatMap {
899-
case None =>
900-
Left(new MissingConfigEntryError(Keys.pgpSecretKeyPassword.fullName))
901-
case Some(p) => Right(p)
902-
}
903-
.orExit(logger)
904-
Some((secretKey.toCliSigning, secretKeyPassword.toCliSigning))
905-
case None =>
906-
None
907-
}
908-
}
909-
secretKeyDetailsOpt match {
910-
case Some((secretKey, secretKeyPassword)) =>
911-
if (forceSigningBinary)
912-
(new scala.cli.internal.BouncycastleSignerMakerSubst).get(
913-
secretKeyPassword,
914-
secretKey,
915-
getLauncher,
916-
logger
917-
)
918-
else
919-
(new BouncycastleSignerMaker).get(
920-
secretKeyPassword,
921-
secretKey,
922-
getLauncher,
923-
logger
924-
)
925-
case None =>
926-
if (repoParams.shouldSign)
927-
logger.diagnostic(
928-
"PGP signatures are disabled, while these are recommended for this repository."
929-
)
930-
NopSigner
931-
}
932-
case Some(PSigner.Nop) => NopSigner
933-
case None => NopSigner
951+
)
952+
} yield getBouncyCastleSigner(secretKey, getSecretKeyPasswordOpt)
953+
case _ =>
954+
if (!publishOptions.contextual(isCi).signer.contains(PSigner.Nop))
955+
logger.message(
956+
"\ud83d\udd13 Artifacts NOT signed as it's not required nor has it been specified"
957+
)
958+
Right(NopSigner)
934959
}
960+
935961
val signerLogger =
936962
new InteractiveSignerLogger(new OutputStreamWriter(System.err), verbosity = 1)
937-
val signRes = signer.signatures(
963+
val signRes = value(signer).signatures(
938964
fileSet0,
939965
now,
940966
ChecksumType.all.map(_.extension).toSet,
@@ -1054,7 +1080,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
10541080
Seq(mod.organization.value, mod.name.value, version)
10551081
else
10561082
mod.organization.value.split('.').toSeq ++ Seq(mod.name.value, version)
1057-
elems.map("/" + _).mkString + "/"
1083+
elems.mkString("/", "/", "/")
10581084
}
10591085
val path = {
10601086
val url = checkRepo.root.stripSuffix("/") + relPath

modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@ final case class PgpSecretKeyCheck(
3535

3636
def check(pubOpt: BPublishOptions): Boolean = {
3737
val opt0 = pubOpt.retained(options.publishParams.setupCi)
38+
39+
lazy val configSecretKey = for {
40+
secretKeyOpt <- configDb().get(Keys.pgpSecretKey).wrapConfigException.toOption
41+
secretKey <- secretKeyOpt
42+
} yield secretKey
43+
3844
opt0.repository.orElse(options.publishRepo.publishRepository).contains("github") ||
39-
opt0.secretKey.isDefined
45+
opt0.secretKey.isDefined ||
46+
(options.publishParams.ci.contains(false) && configSecretKey.isDefined)
4047
}
4148

4249
private val base64Chars = (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ Seq('+', '/', '='))
@@ -91,7 +98,7 @@ final case class PgpSecretKeyCheck(
9198
options.randomSecretKeyMail
9299
.toRight(
93100
new MissingPublishOptionError(
94-
"random secret key mail",
101+
"the e-mail address to associate to the random key pair",
95102
"--random-secret-key-mail",
96103
""
97104
)
@@ -136,7 +143,7 @@ final case class PgpSecretKeyCheck(
136143
"publish.secretKey",
137144
configKeys = Seq(Keys.pgpSecretKey.fullName),
138145
extraMessage =
139-
", and specify publish.secretKeyPassword / --secret-key-password if needed." +
146+
"also specify publish.secretKeyPassword / --secret-key-password if needed." +
140147
(if (options.publishParams.setupCi)
141148
" Alternatively, pass --random-secret-key"
142149
else "")

modules/cli/src/main/scala/scala/cli/errors/MissingPublishOptionError.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ final class MissingPublishOptionError(
1717
val configPart =
1818
if (configKeys.isEmpty) ""
1919
else
20-
s" or by setting ${configKeys.mkString(", ")} in the Scala CLI configuration"
20+
s" or by setting ${configKeys.mkString(", ")} in the configuration"
21+
val extraPart =
22+
if (extraMessage.isEmpty) "" else s", ${extraMessage.dropWhile(_.isWhitespace)}"
23+
2124
s"Missing $name for publishing, specify one with $optionName" +
2225
directivePart +
2326
configPart +
24-
extraMessage
27+
extraPart
2528
}
2629
)

0 commit comments

Comments
 (0)