diff --git a/src/main/resources/update/gitbucket-core_4.31.xml b/src/main/resources/update/gitbucket-core_4.31.xml
new file mode 100644
index 0000000..dcde9af
--- /dev/null
+++ b/src/main/resources/update/gitbucket-core_4.31.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
index d88cdea..83d519a 100644
--- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
+++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
@@ -60,5 +60,6 @@
new Version("4.28.0"),
new Version("4.29.0"),
new Version("4.30.0"),
- new Version("4.30.1")
+ new Version("4.30.1"),
+ new Version("4.31.0", new LiquibaseMigration("update/gitbucket-core_4.31.xml"))
)
diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala
index 005aa8a..da07cde 100644
--- a/src/main/scala/gitbucket/core/controller/AccountController.scala
+++ b/src/main/scala/gitbucket/core/controller/AccountController.scala
@@ -26,6 +26,7 @@
with WikiService
with LabelsService
with SshKeyService
+ with GpgKeyService
with OneselfAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
@@ -42,6 +43,7 @@
with WikiService
with LabelsService
with SshKeyService
+ with GpgKeyService
with OneselfAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
@@ -75,6 +77,8 @@
case class SshKeyForm(title: String, publicKey: String)
+ case class GpgKeyForm(title: String, publicKey: String)
+
case class PersonalTokenForm(note: String)
val newForm = mapping(
@@ -108,6 +112,11 @@
"publicKey" -> trim2(label("Key", text(required, validPublicKey)))
)(SshKeyForm.apply)
+ val gpgKeyForm = mapping(
+ "title" -> trim(label("Title", text(required, maxlength(100)))),
+ "publicKey" -> label("Key", text(required, validGpgPublicKey))
+ )(GpgKeyForm.apply)
+
val personalTokenForm = mapping(
"note" -> trim(label("Token", text(required, maxlength(100))))
)(PersonalTokenForm.apply)
@@ -387,6 +396,27 @@
redirect(s"/${userName}/_ssh")
})
+ get("/:userName/_gpg")(oneselfOnly {
+ val userName = params("userName")
+ getAccountByUserName(userName).map { x =>
+ //html.ssh(x, getPublicKeys(x.userName))
+ html.gpg(x, getGpgPublicKeys(x.userName))
+ } getOrElse NotFound()
+ })
+
+ post("/:userName/_gpg", gpgKeyForm)(oneselfOnly { form =>
+ val userName = params("userName")
+ addGpgPublicKey(userName, form.title, form.publicKey)
+ redirect(s"/${userName}/_gpg")
+ })
+
+ get("/:userName/_gpg/delete/:id")(oneselfOnly {
+ val userName = params("userName")
+ val keyId = params("id").toInt
+ deleteGpgPublicKey(userName, keyId)
+ redirect(s"/${userName}/_gpg")
+ })
+
get("/:userName/_application")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
@@ -771,6 +801,20 @@
}
}
+ private def validGpgPublicKey: Constraint = new Constraint() {
+ override def validate(name: String, value: String, messages: Messages): Option[String] = {
+ GpgUtil.str2GpgKeyId(value) match {
+ case Some(s) if GpgUtil.getGpgKey(s).isEmpty =>
+ None
+ case Some(_) =>
+ Some("GPG key is duplicated.")
+ case None =>
+ Some("GPG key is invalid.")
+ }
+ }
+
+ }
+
private def validAccountName: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
getAccountByUserName(value) match {
diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
index d81a026..7ae412e 100644
--- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
+++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
@@ -14,6 +14,7 @@
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState, CommitStatus}
+import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.view
import gitbucket.core.view.helpers
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream}
@@ -273,9 +274,30 @@
if (path.isEmpty) Nil else path.split("/").toList,
branchName,
repository,
- logs.splitWith { (commit1, commit2) =>
- view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
- },
+ logs
+ .map {
+ c =>
+ CommitInfo(
+ id = c.id,
+ shortMessage = c.shortMessage,
+ fullMessage = c.fullMessage,
+ parents = c.parents,
+ authorTime = c.authorTime,
+ authorName = c.authorName,
+ authorEmailAddress = c.authorEmailAddress,
+ commitTime = c.commitTime,
+ committerName = c.committerName,
+ committerEmailAddress = c.committerEmailAddress,
+ commitSign = c.commitSign,
+ verified = c.commitSign
+ .flatMap { s =>
+ GpgUtil.verifySign(s)
+ }
+ )
+ }
+ .splitWith { (commit1, commit2) =>
+ view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
+ },
page,
hasNext,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
diff --git a/src/main/scala/gitbucket/core/model/GpgKey.scala b/src/main/scala/gitbucket/core/model/GpgKey.scala
new file mode 100644
index 0000000..d4859f0
--- /dev/null
+++ b/src/main/scala/gitbucket/core/model/GpgKey.scala
@@ -0,0 +1,29 @@
+package gitbucket.core.model
+
+trait GpgKeyComponent { self: Profile =>
+ import profile.api._
+
+ lazy val GpgKeys = TableQuery[GpgKeys]
+
+ class GpgKeys(tag: Tag) extends Table[GpgKey](tag, "GPG_KEY") {
+ val userName = column[String]("USER_NAME")
+ val keyId = column[Int]("KEY_ID", O AutoInc)
+ val gpgKeyId = column[Long]("GPG_KEY_ID")
+ val title = column[String]("TITLE")
+ val publicKey = column[String]("PUBLIC_KEY")
+ def * = (userName, keyId, gpgKeyId, title, publicKey) <> (GpgKey.tupled, GpgKey.unapply)
+
+ def byPrimaryKey(userName: String, keyId: Int) =
+ (this.userName === userName.bind) && (this.keyId === keyId.bind)
+ def byGpgKeyId(gpgKeyId: Long) =
+ this.gpgKeyId === gpgKeyId.bind
+ }
+}
+
+case class GpgKey(
+ userName: String,
+ keyId: Int = 0,
+ gpgKeyId: Long,
+ title: String,
+ publicKey: String
+)
diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala
index 577c3ee..ce1f96b 100644
--- a/src/main/scala/gitbucket/core/model/Profile.scala
+++ b/src/main/scala/gitbucket/core/model/Profile.scala
@@ -59,6 +59,7 @@
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
+ with GpgKeyComponent
with RepositoryWebHookComponent
with RepositoryWebHookEventComponent
with AccountWebHookComponent
diff --git a/src/main/scala/gitbucket/core/service/GpgKeyService.scala b/src/main/scala/gitbucket/core/service/GpgKeyService.scala
new file mode 100644
index 0000000..3e10b93
--- /dev/null
+++ b/src/main/scala/gitbucket/core/service/GpgKeyService.scala
@@ -0,0 +1,29 @@
+package gitbucket.core.service
+
+import java.io.ByteArrayInputStream
+
+import gitbucket.core.model.GpgKey
+
+import collection.JavaConverters._
+import gitbucket.core.model.Profile._
+import gitbucket.core.model.Profile.profile.blockingApi._
+import org.bouncycastle.bcpg.ArmoredInputStream
+import org.bouncycastle.openpgp.PGPPublicKeyRing
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory
+
+trait GpgKeyService {
+ def getGpgPublicKeys(userName: String)(implicit s: Session): List[GpgKey] =
+ GpgKeys.filter(_.userName === userName.bind).sortBy(_.gpgKeyId).list
+
+ def addGpgPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit = {
+ val pubKeyOf = new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(publicKey.getBytes)))
+ pubKeyOf.iterator().asScala.foreach {
+ case keyRing: PGPPublicKeyRing =>
+ val key = keyRing.getPublicKey()
+ GpgKeys.insert(GpgKey(userName = userName, gpgKeyId = key.getKeyID, title = title, publicKey = publicKey))
+ }
+ }
+
+ def deleteGpgPublicKey(userName: String, keyId: Int)(implicit s: Session): Unit =
+ GpgKeys.filter(_.byPrimaryKey(userName, keyId)).delete
+}
diff --git a/src/main/scala/gitbucket/core/util/GpgUtil.scala b/src/main/scala/gitbucket/core/util/GpgUtil.scala
new file mode 100644
index 0000000..5d5edb4
--- /dev/null
+++ b/src/main/scala/gitbucket/core/util/GpgUtil.scala
@@ -0,0 +1,61 @@
+package gitbucket.core.util
+import java.io.ByteArrayInputStream
+
+import collection.JavaConverters._
+import gitbucket.core.model.Profile._
+import gitbucket.core.model.Profile.profile.blockingApi._
+import org.bouncycastle.bcpg.ArmoredInputStream
+import org.bouncycastle.openpgp.{PGPPublicKey, PGPPublicKeyRing, PGPSignatureList}
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
+
+object GpgUtil {
+ def str2GpgKeyId(keyStr: String): Option[Long] = {
+ val pubKeyOf = new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(keyStr.getBytes)))
+ pubKeyOf.iterator().asScala.collectFirst {
+ case keyRing: PGPPublicKeyRing =>
+ keyRing.getPublicKey().getKeyID
+ }
+ }
+
+ def getGpgKey(gpgKeyId: Long)(implicit s: Session): Option[PGPPublicKey] = {
+ val pubKeyOpt = GpgKeys.filter(_.byGpgKeyId(gpgKeyId)).map { _.publicKey }.firstOption
+ pubKeyOpt.flatMap { pubKeyStr =>
+ val pubKeyObjFactory =
+ new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(pubKeyStr.getBytes())))
+ pubKeyObjFactory.nextObject() match {
+ case pubKeyRing: PGPPublicKeyRing =>
+ Option(pubKeyRing.getPublicKey(gpgKeyId))
+ case _ =>
+ None
+ }
+ }
+ }
+
+ def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
+ new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
+ .iterator()
+ .asScala
+ .flatMap {
+ case signList: PGPSignatureList =>
+ signList
+ .iterator()
+ .asScala
+ .flatMap { sign =>
+ getGpgKey(sign.getKeyID)
+ .map { pubKey =>
+ sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
+ sign.update(signInfo.target)
+ (sign, pubKey)
+ }
+ .collect {
+ case (sign, pubKey) if sign.verify() =>
+ JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
+ }
+ }
+
+ }
+ .toList
+ .headOption
+ }
+}
diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala
index 16d6e8e..e875a2b 100644
--- a/src/main/scala/gitbucket/core/util/JGitUtil.scala
+++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala
@@ -1,6 +1,6 @@
package gitbucket.core.util
-import java.io.{ByteArrayOutputStream, File, FileInputStream, InputStream}
+import java.io._
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
@@ -76,6 +76,59 @@
)
/**
+ * The gpg commit sign data.
+ * @param signArmored signature for commit
+ * @param target string for verification target
+ */
+ case class GpgSignInfo(signArmored: Array[Byte], target: Array[Byte])
+
+ /**
+ * The verified gpg sign data.
+ * @param signedUser
+ * @param signedKeyId
+ */
+ case class GpgVerifyInfo(signedUser: String, signedKeyId: String)
+
+ private def getSignTarget(rev: RevCommit): Array[Byte] = {
+ val ascii = "ASCII"
+ val os = new ByteArrayOutputStream()
+ val w = new OutputStreamWriter(os, rev.getEncoding)
+ os.write("tree ".getBytes(ascii))
+ rev.getTree.copyTo(os)
+ os.write('\n')
+
+ rev.getParents.foreach { p =>
+ os.write("parent ".getBytes(ascii))
+ p.copyTo(os)
+ os.write('\n')
+ }
+
+ os.write("author ".getBytes(ascii))
+ w.write(rev.getAuthorIdent.toExternalString)
+ w.flush()
+ os.write('\n')
+
+ os.write("committer ".getBytes(ascii))
+ w.write(rev.getCommitterIdent.toExternalString)
+ w.flush()
+ os.write('\n')
+
+ if (rev.getEncoding.name != "UTF-8") {
+ os.write("encoding ".getBytes(ascii))
+ os.write(Constants.encodeASCII(rev.getEncoding.name))
+ os.write('\n')
+ }
+
+ os.write('\n')
+
+ if (!rev.getFullMessage.isEmpty) {
+ w.write(rev.getFullMessage)
+ w.flush()
+ }
+ os.toByteArray
+ }
+
+ /**
* The commit data.
*
* @param id the commit id
@@ -99,7 +152,9 @@
authorEmailAddress: String,
commitTime: Date,
committerName: String,
- committerEmailAddress: String
+ committerEmailAddress: String,
+ commitSign: Option[GpgSignInfo],
+ verified: Option[GpgVerifyInfo]
) {
def this(rev: org.eclipse.jgit.revwalk.RevCommit) =
@@ -113,7 +168,11 @@
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
- rev.getCommitterIdent.getEmailAddress
+ rev.getCommitterIdent.getEmailAddress,
+ Option(rev.getRawGpgSignature).map { s =>
+ GpgSignInfo(s, getSignTarget(rev))
+ },
+ None
)
val summary = getSummaryMessage(fullMessage, shortMessage)
diff --git a/src/main/twirl/gitbucket/core/account/gpg.scala.html b/src/main/twirl/gitbucket/core/account/gpg.scala.html
new file mode 100644
index 0000000..e453124
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/account/gpg.scala.html
@@ -0,0 +1,39 @@
+@(account: gitbucket.core.model.Account, gpgKeys: List[gitbucket.core.model.GpgKey])(implicit context: gitbucket.core.controller.Context)
+@import gitbucket.core.ssh.SshUtil
+@gitbucket.core.html.main("GPG Keys"){
+ @gitbucket.core.account.html.menu("gpg", context.loginAccount.get.userName, false){
+
+
GPG Keys
+
+ @if(gpgKeys.isEmpty){
+ No keys
+ }
+ @gpgKeys.zipWithIndex.map { case (key, i) =>
+ @if(i != 0){
+
+ }
+
@key.title (@key.gpgKeyId.toHexString.toUpperCase)
+
Delete
+ }
+
+
+
+ }
+}
diff --git a/src/main/twirl/gitbucket/core/account/menu.scala.html b/src/main/twirl/gitbucket/core/account/menu.scala.html
index 07fc46b..b7ed72d 100644
--- a/src/main/twirl/gitbucket/core/account/menu.scala.html
+++ b/src/main/twirl/gitbucket/core/account/menu.scala.html
@@ -26,6 +26,11 @@
}
+