Browse Source

GPG: handle extended private key format

Add detection for the key-value pair format that was available in
gpg-agent for some time already and that has become the default since
gpg-agent 2.2.20. If a secret key in the .gnupg/private-keys-v1.d
directory is found to have this format, extract the human-readable key
from it, convert it to the binary serialized form and hand that to
BouncyCastle.

Encrypted keys in the new format may use AES/OCB. OCB is a patent-
encumbered algorithm; although there is a license for open-source
software, that may not be good enough and OCB may not be available in
Java. It is not available in the default security provider in Java,
and it is also not available in the BouncyCastle version included in
Eclipse.

Implement AES/OCB decryption, throwing a PGPException with a nice
message if the algorithm is not available. Include a copy of the normal
s-expression parser of BouncyCastle and fix it to properly handle data
from such keys: such keys do not contain an internal hash since the
AES/OCB cipher includes and checks a MAC already.

Bug: 570501
Change-Id: Ifa6391a809a84cfc6ae7c6610af6a79204b4143b
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
tags/v5.11.0.202102240950-m3
Thomas Wolf 3 years ago
parent
commit
bdc48aeac7
20 changed files with 2157 additions and 94 deletions
  1. 3
    1
      org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
  2. 41
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc
  3. 42
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key
  4. 42
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc
  5. 45
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key
  6. 30
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc
  7. 32
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key
  8. 23
    0
      org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key
  9. 155
    0
      org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
  10. 3
    5
      org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
  11. 38
    45
      org.eclipse.jgit.gpg.bc/about.html
  12. 12
    0
      org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
  13. 12
    0
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
  14. 16
    35
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
  15. 3
    4
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
  16. 6
    4
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
  17. 121
    0
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
  18. 826
    0
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
  19. 110
    0
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
  20. 597
    0
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java

+ 3
- 1
org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF View File

@@ -9,6 +9,7 @@ Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
org.eclipse.jgit.gpg.bc.internal;version="[5.11.0,5.12.0)",
@@ -17,6 +18,7 @@ Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.junit;version="[4.13,5.0.0)",
org.junit.runner;version="[4.13,5.0.0)",
org.junit.runners;version="[4.13,5.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true
Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true,
org.eclipse.jgit.gpg.bc.internal.keys;x-internal:=true
Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
org.hamcrest.library;bundle-version="[1.1.0,2.0.0)"

+ 41
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc View File

@@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBGAHBLQBDACsS1vFqE3qgKD2R5X9n90Gz8bucwwvJWIqaHDsVoAtF6IcKIDo
1hQC9YksTQYl/L7BsMDdmjyEbWRfzW4ory5596d342Hl6g7ZB5jJR5kJJdhy2MCJ
BUiMy/724Fr/Dz8PNPcEoULz9ZH7HEaPRKqWWEQDUCq5ak0MfLKXtWVUBgsY5Mry
29d/GLJvnxZ5v16PK+P4oqZ7vh7FWJPlqPK2TCZ6s1rYfWlu9XbHOzwXwozVg7IX
tfFq4Rij4c0sg0S0GY8hGAlnOpRc/6J2S41Y8p3WqND6r1LPDQUFnNCKXVoHUGeK
X9U5iAP7pxZSuonsFCqr3CDGxr+kKUpbfZeLrqTA4lBUK7T6w6Wq0qHosCYUU7YC
GZjlEeCZBRWNfeq45LKlhdNUxHWWgaBsgWaaDmpFWaivblmQGOvmSv1nJMNmedRs
DSF51nsJnkQceprsvThSa6qJwEYi7pj6L9HO2UGgJLCb3dL5VTQih2gdhghckUSB
okUkvqBvvdiP2nEAEQEAAbQdVGVzdGVyMiA8dGVzdGVyMkBleGFtcGxlLm9yZz6J
Ac4EEwEKADgWIQRPCAvglun3I2Bs1iITM2XBzCpwbgUCYAcEtAIbAwULCQgHAwUV
CgkICwUWAgMBAAIeAQIXgAAKCRATM2XBzCpwboiBC/493+ruANV2eiro8MY8wZ3Y
gdjp3pHBSg9RK74SIh95J+MW5qzPwkU+vHd8l0+aj9e1sDQb5BFcFk/Z1ioI3TDW
B4vYWoMkdN932fJ/LcIlhOGjWwSNFZphbYmJzrAwUTA499yx3jt9Dg+vSU88S+8S
FzYe6CBNt+PqDCbk6Gm+ZcVpR+elq/QJeyhdDzCCrrfNXwPwsVGAM61Z8SvdvNKE
DA5gHXRsOKf8fu8lqW2Ay0MCvgsZLMIGOMDPCyBUd1bhlU0p18V6D6wdatfzu9gR
X/k36HJyqB2cHh89/F2KdBSonRVRJOvHc/88zEeRFkiV5pUyrXv40l099+5dvA+2
h4ODftY7ZbR22k4iX5rqj2BRow3H+N5lTIWgiADPUl+H8z4ZY5G+LWk9Xms3o1G9
DmEepM3ma1pg4sZbxf0iStikch7aPvL/HgGRPJnDxA/W4KJxqmSw9TTMH/6XHq3D
ah5Z1lbcylChgrFLFVJi+shnLTZSYttTeKOIqTPi0765AY0EYAcEtAEMANS23tqF
Dr69wz0AaT7tjoccT/WlSO/gxd80ShMr4vbr21PZp8qGklFmlcrSrMDRwfXY04x2
qxHR/Kf+hCD5gNvg8kh/yH2lQRcvekzQ4/rLmSXBfGOFg+LioQQ3CZJ1MZyIHzu5
YVZ2pqALfJwJSw9P5Z340y8sq8AOPaJ+cpIC0rYBp9BUAmz9IeLVT7fUc6CjaWBo
++E8H+9FyZC71RIPNcCvY+24Qky8ms7nw4hA47Dlht1pqL8dzOggCnohuSYMCXs2
YPLvDGdZMg7GgQ3AyZawDmjTxFWt51VU5hunGfGiC5Aock8rVHSYsQzUFjVBSR+Y
Zy+c4noxZD1eRfb8KdFnrewyVqGKFtc/JwA61qhhyYFe5AWMAFtudjGYG0WiTP82
CmFFc1Qsvyls9G2yMkLuay5wsdIJMnRW9XwBzwxm0mdZI6D3nSbWjPUUfRcGBY8C
Hqpc736G+UzMevZtorwy/5Q6D8v+Obrk02DIDKa6CJ7g7dTwK0I/fleJlwARAQAB
iQG2BBgBCgAgFiEETwgL4Jbp9yNgbNYiEzNlwcwqcG4FAmAHBLQCGwwACgkQEzNl
wcwqcG6mYQv8CFIVGj7/Qnr+wmviMzm8+B4WwQIUHGryqv9hnfp9hLOXMFmNuEDl
QYkHVChWO7ehrR3fpvpebhcieV19skf/WO8xm0pGSXyjV2/0/bVhXq01xesXHH9r
4aFxsCu0E8M9fZVAHP7NBr4A67knQ4EHRF6Rwml2ba6Zt2oP15IHvsAq/2B3f8ar
5sUau4zM1cItG3tg49rbYr6V71HdgkWA22+EkbXL/Qq3haY/er2dIGc73lu8t7oQ
msGK4LSAGc2661wMvJ6w6feCagkXAtrqyxodhSLoWgF3i0QVQnMbgmYWKEK2B6YA
g669CZCCXJF+9Ebq+PP/d3Cy/k9iUmWDh72C7iL136kYZt+71b+yOmlDRT9l6DvU
FP3bhRZWomOt3F3aP5mAdbwrP1NbvlxTYUAf++nUPdpr0Jrvgi67/VHVjaUtVh/K
gVQ2C+4Cp/fllxXXKQMPcC8dD1x/AL6ytDzPu099ETMULntgbt7A5Lsd/fFScnF3
ZNx6wjRReIvT
=8E/K
-----END PGP PUBLIC KEY BLOCK-----

+ 42
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key View File

@@ -0,0 +1,42 @@
Key: (private-key (rsa (n #00AC4B5BC5A84DEA80A0F64795FD9FDD06CFC6EE730C
2F25622A6870EC56802D17A21C2880E8D61402F5892C4D0625FCBEC1B0C0DD9A3C846D
645FCD6E28AF2E79F7A777E361E5EA0ED90798C947990925D872D8C08905488CCBFEF6
E05AFF0F3F0F34F704A142F3F591FB1C468F44AA96584403502AB96A4D0C7CB297B565
54060B18E4CAF2DBD77F18B26F9F1679BF5E8F2BE3F8A2A67BBE1EC55893E5A8F2B64C
267AB35AD87D696EF576C73B3C17C28CD583B217B5F16AE118A3E1CD2C8344B4198F21
1809673A945CFFA2764B8D58F29DD6A8D0FAAF52CF0D05059CD08A5D5A0750678A5FD5
398803FBA71652BA89EC142AABDC20C6C6BFA4294A5B7D978BAEA4C0E250542BB4FAC3
A5AAD2A1E8B0261453B6021998E511E09905158D7DEAB8E4B2A585D354C4759681A06C
81669A0E6A4559A8AF6E599018EBE64AFD6724C36679D46C0D2179D67B099E441C7A9A
ECBD38526BAA89C04622EE98FA2FD1CED941A024B09BDDD2F955342287681D86085C91
4481A24524BEA06FBDD88FDA71#)(e #010001#)(d
#208024A31FF0F6B3E5E91F2ED58572F1A67F1D9AD9290991BF732D1DFFE134E058E5
9BE459478CC5D42058997CF7EC79E55A9CBF10A9AAC761E04A85A5AA0A07DAE61DD0E8
36311534EE606D5392B42D8DEB7824B594280FDB2950D39886B58F0D24CE15F2FF88BA
819B8F4566202B57A9F5C6743862FA80E7429C83CEA57B189ABE4AE657B28DAF7D6EA7
6CA89635B9B6232EE14779452D636B919E707B92B13DA3229133A953DAF021E0928B83
75EDEE98163C2189E22CE9A236C3D0EABD2608DAEF09211B2C77FFE9158A95F8EF2750
221C5ADEDAED0446DC0E4CD8D38AD897D44FA3915934B6CF03F489DFAA6D939AB9F8EF
1C2A0CDCFC3F2207D63A9EB55B09A0A45323D5F59AE4A9D48E819E98E53D04F022905A
9C4D137F32CB33A974F202B0D3AD4AC64CFBA2A4C18650B671AB753D1D3BD7C4FCC8D2
0F85D1876D89A3D94C77423778C08BDF8FBA23A8501D766FC1B4D51F2D4BB4C27B8491
CC2595FF54034F4F192D668C1934D292752A4E44C95135D29449B75928BAF1A2389ED9
#)(p #00CCD74AC0DC1CC46052F784DB19545A09FF904846247BAD1AFA5E00CE84A4DA
BFCD3BCA175508C068553226DBA49EDAFBCC33CF2A914F9006326FCB62C0473B1E93F6
DCF89A24006B090834E5686464A8C216B70AD797732E671ED78CD3E922161069E46BA7
489F2A79CE46BDC4E6F5FCE97C3C9DC59399212235C53246609F8B7FDBF2AD191B3FB4
4CC59760BA6D2029541811D1E9B72DC2ADC98513589A9715C82EE88ADF9000A41512C9
6D491D2A30269FBFCD9CF5D2F275A1DBFFEEB72BE5#)(q
#00D7532ABA5F442A994ED8290AA71EAAB6F8137FE3F01488298637084157972D31EA
E9F495B4360A6E92ABA7C2418A5960DF29B8C8146CC7D1DF1201C17509D7315B6ECF86
F0A73C9F5B48D96F2108DD323FAE6BF897A8CB773EDCF5C82E203813945405F414E3F2
99EEDE43EE6259FDED1C01B47C20F67AC488209585FE6FB7D958AF5EF567737E1ACCB4
E293724BE8AB6159CD5A6603FFEFC2DBC30FB6EAF647DBE7D9664ED0BBA1C4A2268AE3
DE0850572E145BA811EB2087E1E26490F9439D#)(u
#00A8026DB6685170EC05DA3574451678718501489843227BCEB772FDB86B20AB2F2A
00B790A2373FD5DF7AD2CAA2E3896C4C9CBA08581F3644DF67743FA4BE1153421F8FB2
47F0EFB64C566CB7E361BAB21CCAF029B43B7172232D11D14912BC035E17FB8BC663CA
6766E6D2531F87AF378896A2AC7D98824AA8F51C5D6C11B7DC266209BCD3B23597F02B
A317CCAACC979402F84E47F3D143187F9739FE9A6C71829CC94E4003D70CFFA33AC0FA
A12776EFAFFB8261ABE664C6A6FAE623D3678F#)))
Created: 20210119T161132

+ 42
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc View File

@@ -0,0 +1,42 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBGAHBAUBDADAzIW1FhCcQmP/NDhzXoeRQ+DNACqTed7eEhqm3rowkW4wKi56
v1UxFR0ZoA3LoT1oQQjiL3IS2l4/qpBR3JQhMFH3pl7yBsCIrN7JvZfAvxq2Ud4e
YbdonY8mv/yCLq+nkTWlHkWGCppKMm6DupEUw5CFUCiptPXIxikU0uQYB7VRtXhh
Q1RGdsv6mcOwMIh7hj9flTrX025x0vRypjqgDR05RuM3hMMpJDGMAuf51w/lbRl9
dAsJzFzg2cf+8qv92Gx3RuP3a3yl6pEuKnkpddC47lj2pvuhWZBf2sXZMyPvMIvA
Dve4GIVj6k+wXE3DMp0xMy0Fvaxw5OORPxUAKNBR/BgjoRbkbjm+rJZzviu/XVPR
42+78isvsa+lnEAmTeoTqiz9nlTTPN+6JjwJXYn2LuiFM8XzNPJwNmd/86lW1qbP
DxZHhD9jjeXZNHUBUCKTIj2Rs2uFa0xrALdhhhGEao9JlVcUqz/Tw85qC+DlzSa3
3re5C6wGe2pnW3sAEQEAAbRGVGVzdGVyICjDhG5kZXJ1bmdlbiBpbiBHUEcga8O2
bm5lbiBGb2xnZW4gaGFiZW4hKSA8dGVzdGVyQGV4YW1wbGUub3JnPokBzgQTAQoA
OBYhBPidC43dWzUvOqsRbzx6+72u3pDoBQJgBwQFAhsDBQsJCAcDBRUKCQgLBRYC
AwEAAh4BAheAAAoJEDx6+72u3pDogeAL/iNn1aKxA7pKmucyuzcbzvUjtcbbqFL8
lOLdRxkrQNCDMb+wHgGY1UqJ6wsDruhV+TdPLzUXHpCl731MeLxZyIENr4wnjTBf
Cr4SU8eFUkusVf3aWK3rlk54W50EkfBjMvDVavRKNkVbCWAAwXZ7mTRf3UlWxg+F
9Sq4j2P/hEZIznKV3y7zXLDYg0OpMZLgbo3si0CD19/1T/8Z9C680qSwyAiPtjRo
vfJYqZFQc+ZH7j1Hmvg68d2Qwrkg/WMfOGoTLZq/6PQcM5leQBAodcS1t7C8o1JQ
6D+f2gLHpMfFdUKh9TkmvnKYI20TWUVm9XQLqyAHsOn2vRMUhydcZ8OP103TKmP4
mbpgiyp4i4S/7XofHSeFBrbdqt73ebubESuZVXNjTuuSUjH8Jq5nHq22ZrmotSd0
FNwc9qQmwPG/gmOGq3rUdT/KzUVntc66QN/+hMhFDYMRJsjJhhyszvGuuBp6vBzI
lMZqx5jqOaI9ON0m0o8CKC50sKdJ25G3wLkBjQRgBwQFAQwAqMXwgfSaIM+eSQWs
xb4Sf2aCr/RZi5wzZz89lSomMcblqtCpuHv9/C1PSd+N/D1yJKzPChbDjHh6B6gc
4OUvuDKHmxK+oAiURpvR+yJEdbSEYwiBhfAUD6u2q3IfY5PpyyZT3NjZ9EY8FpOX
wpgdSOdSiZVZfZt0xUPsGbW/xP1yVR1NHYLuZX5P5oTCvyNJyP8zQQmToamJsvzR
v9r+2sa5di9roe53kZwq24VjIvTDOOE4xoYEXk5UD83u1LA++9Nfdisxxts1bxgj
w1ThO/IRTyY5y5bKSQPskYFD3eVz+azjVurqhbj6ep69mR2X2gjLCetz6G1l4PU2
R6wcqVG6gR6XpGFPsN+M2JRtbgKrtMElA8egJIMMpH4hdXYqIqmpe/31zXhClHkO
99EbBpk6OawZC+MnreQFN4NIK5uO2aLi+3KL1FFnNlFCXkh/8afbt4+6rHcWC6En
q5W0ZkLZnpjdFOF5NPTinAdei+14gnf2QhIlFLeMHvqiVEXvABEBAAGJAbYEGAEK
ACAWIQT4nQuN3Vs1LzqrEW88evu9rt6Q6AUCYAcEBQIbDAAKCRA8evu9rt6Q6DcX
C/4orMX1YBZNJK5hLLdjfk45EsQDfCnhf8H991xd0Rq4VPJP6bvzikSdOn9bTUEz
AAhA4JnFu9AMTh8ioOA7ZtViIccplFBivsxi3rAVrQvmCfoP2AdHfG/jB6D9uWGs
MV5/o1p93Hr0ReO73HK6G4Q3FbJOG3fg6wTcMYyyEQrD5g4IQhKiIhudUlSkKUkA
9hWKlXSLw3Yx/S2Nq5Ye+Pqr4CU7UFOTCsBIH4Ky+6gLTmP6esPx0k8vXLcOjaCk
ENcLi0OaL/AgfATH/InN3wzrx2AFfU6eQdEG7HS+402eHl0fmWwMGV+SCsLl+2hx
AguLFwjetuVrroc/d+XeZdTcpr/2vojsr4UgbkH8Pa2KrGIpK7V85nSOeVbpDUru
tuimIRSxIQ6GDF2c7Ih5yBy+JPV47gppSV/GgHPgrOlebeqy4sytshRiEYw/nJzZ
LKBaG6gykN+6MeV6+A7c1BlCYpyi2vcyvouU+l3/Z9gR7vY+Oj1eAaxrqeTFf++3
qnA=
=03Wd
-----END PGP PUBLIC KEY BLOCK-----

+ 45
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key View File

@@ -0,0 +1,45 @@
Key: (protected-private-key (rsa (n #00C0CC85B516109C4263FF3438735E8791
43E0CD002A9379DEDE121AA6DEBA30916E302A2E7ABF5531151D19A00DCBA13D684108
E22F7212DA5E3FAA9051DC94213051F7A65EF206C088ACDEC9BD97C0BF1AB651DE1E61
B7689D8F26BFFC822EAFA79135A51E45860A9A4A326E83BA9114C390855028A9B4F5C8
C62914D2E41807B551B5786143544676CBFA99C3B030887B863F5F953AD7D36E71D2F4
72A63AA00D1D3946E33784C32924318C02E7F9D70FE56D197D740B09CC5CE0D9C7FEF2
ABFDD86C7746E3F76B7CA5EA912E2A792975D0B8EE58F6A6FBA159905FDAC5D93323EF
308BC00EF7B8188563EA4FB05C4DC3329D31332D05BDAC70E4E3913F150028D051FC18
23A116E46E39BEAC9673BE2BBF5D53D1E36FBBF22B2FB1AFA59C40264DEA13AA2CFD9E
54D33CDFBA263C095D89F62EE88533C5F334F27036677FF3A956D6A6CF0F1647843F63
8DE5D9347501502293223D91B36B856B4C6B00B7618611846A8F49955714AB3FD3C3CE
6A0BE0E5CD26B7DEB7B90BAC067B6A675B7B#)(e #010001#)(protected
openpgp-s2k3-ocb-aes ((sha1 #84A4A8051974D94E#
"26420224")#914E6847983627126D1CDF93#)#DEB66FA3201F91591F688F5D2B1B79
39FD75F58A962C227BC739C6F2F814ADE5115BD85B2E55427153CEC82612F0C5BBE8B9
71A0E5A6B796111B6B1A03C4C926825F03B871CBFE0F64BD0F0CC65EA34E718BA823BD
136D78C9E88CA1733DFC8D6A38830274322A589BC522A2A824FCEAF453523CDD9BC391
EEF1355470C110E9A92681DA0C61563465D5238CECCA2D6CFA78FFDFBDDA17A308D6E1
3B1858890EF25A7655F22FE6305AA0129DE5A353B657065E608A616A23C6AF561C4472
5AA705E55343E9C728641BB63C64F804F76A4C5008CE5FFBC09F03B632B42180425D28
9DC1201D91B1989627EE5930E6EF2F6606108B2F048934A9D79DB4834DD950C4A2013C
A40B50EE54FA9E3CCB20C210244BFACA795494A1FCFF35856ACF63214A0498ED894BAB
FE80CC24D8A478AD08D0BE8CDC8F357FB7F28A30B87540B9B4970D6EA0AEEF46A2549F
BA43A98FAD75B4228108DB50D1C3654422E24B4C7754673A66281BB283CD6A1EA8E64B
97DC9083C62034BF7FBCD193830F8FEC3589673B864E50EF7AF4DEE046BD26041E2925
170EB7B6DC6060E78309CC8A136AE9CE44D3B4EBDEE4479482464D0D23C13529184021
795557323D353A70CC710EC2A79C66E860095C082E40724867A9ABBFD3407B2F92CB2A
D0D95CC8DAC2FB2C0187B3BB09AEDF869AF1969BA641027D4D5DAA31B1DA5822D40A5A
7FAD1C054C02CF8F8F692B1C45C879299C0E9D5E5A165F6C22DEEBB8C16FFB91F381C4
8FCB209657A7BF9268BD34808D0A9D3D6F50F7026BC297FD3A08790B8EB5CC0291246B
16E4B50A7E9630B33F59B5EA24EEA396F07AEFD0C262BE9915CB32D5F03673CBD3D20A
831FDF55B5BA3D03440A8E1A331147A8AE0760EC593EDC881F5F0A04F4FCBC80C1531A
4DE71D014E3612C2C679BEF3AED59F358ABA5731FF80FA15EAD2CB95AC548F6AB0FD7B
BBBB2CB63DFFE9E672605B7F54EEA4B4C046C4CC8036F2F76260CF068D232A40F492FF
9648CC7459F0F46FEABA3D62B9F421B0F8A1BF914E41702540213848345498AA13F989
49EFC2888D3720DC34D20634472FC3A194F1403C1609C38A020F7E47F3205CA5C0CB50
26270083ED153BF97375407514BA15D92808A8C10F8160880F6981BE53294292E4EEE8
D215E7854FC79016B64984BBBAD2E99EED8D66B25575183C279E4DAFCB63F1067FA2B0
0888A9C226D4846376520720FC1E947A93A1D32444F78B2F4EB836A6F8C685C1D82958
E31560C3FC861D2B68B889E1B5EE0476B914DFE316411F750D252F24076E53557AD5F0
4050E5E839B33E5B8AF16FDD9FE033B39796A52DE8AF65375966D4DB137C85C800B5B9
0E29434E4B215DE35E60C85391DFDFA572C6F9747A0EA0964236FBB3B04394D9DF0694
6E4CC9CBCE352382908148D265293C6EADE7C2AED6F5AE#)(protected-at
"20210119T160858")))
Created: 20210119T160837

+ 30
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc View File

@@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBGAHViwBCADaE67a5U8oqNjqg//SmlNLd+MrFIccHsg+pkSDhF6ipjZEXdFu
oRQ116tH/qY1SzsHw/TMLujYeTBW0KwQ5E2+TWagckL8pJpDt7ZC08CTK6u0Xvjy
BSqy/t29NPTuQxxxqRx5gq91mtuo8v00fQmqkbFUgkVfEOKOv4qe2tlaR5pTvpmV
VboXOls87RPgP/X665kamHjsywrsDpZ5FbvPS8E2kKdYXqeHaiEU4i0Bizjx3diK
ilPEIxxl8zgDsROXyKagCy0KOOajBqhFhStQH1soIzvk8aG/9eItKmTa2v1BD2mV
UlZNQ9ZfsnXx3QIBLmA3jugH69rkcekHRCWPABEBAAG0HVRlc3RlcjQgPHRlc3Rl
cjRAZXhhbXBsZS5vcmc+iQFOBBMBCgA4FiEEJJx/+EvV3v35cJ+Jx5r/T8UxegwF
AmAHViwCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQx5r/T8Uxegx+hQgA
pIy+E58aWh9FIHW/POqLB/y3v/GbYdIbzk9ro0FAXQ2tQsoGQbsuLckJVIlyja7n
oQX23OmWTOc2tj/Kpy9lZ2ButW43FaMSiLh1G/VtM5pqJ9yHFdb1z7Q+LHMhhB7w
RKOoNSEgkwtxv4LAkKz5t/BrDU0hvDPxWPqCSRvSAEE6qIY0fa3mLf9ijy3gLlCd
hBayUC53W0tL7gLHgprTM/fX7b1pDab8beorroPHs5XzcYUBleaCGmEYbtV2eXPQ
5irOXOC/D/E8vOfOZwhOhFOZk9b4UnhFK4OCfpKIzIooc6tlboVPhdx7jb5DkXQo
rozfavEvAPx8INi9KNmdBrkBDQRgB1YsAQgA6q/O6mAPB0kj3pnpuIdJC8EumgKm
7W8rv+ZfRGePg+IEEm5DeFKtfWl70YH33nGmwnWB95wO5412JCNC174z5LKDBbz5
PWT/yTNnmjooxj6G4p/YVwXYJkvfaDP+kQnYgJAybpeqTa30tES0eqvI0J9aQo1h
GSMRCkE6QMV45IMj6gH9rptQv9e8U6gbnwBPxWPG2FH5rsGIGQGzIEmGxmKRyxXm
YDU4f7oWPHSg1ikQqCzAxxCBxeCOaid3acLK8//TOwF/Do8GPJbcupEDqsgbFNGM
BDWtmkmxjrLntlU+dvIPcsBxdBUrrADiJ/k7EfFv3kHfLfdAonSdKZL85wARAQAB
iQE2BBgBCgAgFiEEJJx/+EvV3v35cJ+Jx5r/T8UxegwFAmAHViwCGwwACgkQx5r/
T8Uxegy4+AgA1bzFKpsqkwrjZKDCCT759xeuUbxnYE9kBJgFSVuhn7fUbB4MoHx4
shBptx7iBOdxxT7yC0oaDPhbiIkttb/c5W0f6JuLr08JpjkFfkrWF+dMcVrtXwPx
i/30ccV98qWJDCBunyeCwBNie1Ck+qXMxm3FYy4qIbftMQ7mG6KKN6eFlbxu8B6M
p93DFUvycGH9CWz0yJcho7KT0NSSyoLZhJz2uxRe1BwGMV20O9AG9yicsU0/uJxY
a2Hble8NkH54XDuZkrsBaAb/o8UsWP7AJdYYsb904UZDIZNRfjWapOmODnlnK8Ta
Q8pyYRGS5of1SapatMfpQZF6hdsamnTH6A==
=guSE
-----END PGP PUBLIC KEY BLOCK-----

+ 32
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key View File

@@ -0,0 +1,32 @@
Key: (protected-private-key (rsa (n #00DA13AEDAE54F28A8D8EA83FFD29A534B
77E32B14871C1EC83EA64483845EA2A636445DD16EA11435D7AB47FEA6354B3B07C3F4
CC2EE8D8793056D0AC10E44DBE4D66A07242FCA49A43B7B642D3C0932BABB45EF8F205
2AB2FEDDBD34F4EE431C71A91C7982AF759ADBA8F2FD347D09AA91B15482455F10E28E
BF8A9EDAD95A479A53BE999555BA173A5B3CED13E03FF5FAEB991A9878ECCB0AEC0E96
7915BBCF4BC13690A7585EA7876A2114E22D018B38F1DDD88A8A53C4231C65F33803B1
1397C8A6A00B2D0A38E6A306A845852B501F5B28233BE4F1A1BFF5E22D2A64DADAFD41
0F699552564D43D65FB275F1DD02012E60378EE807EBDAE471E90744258F#)(e
#010001#)(protected openpgp-s2k3-ocb-aes ((sha1 #3E87FF0806B054D6#
"26420224")#7E8282B83522F91C53D76805#)#70B1997D514FF5800155A90FD785E7
7DC2D782C48FC9BE44D0192C0AD56804468C910A202191EAD077B5542C95FE72BCC450
0C2A8E8313C0CBD6C881236AC13E0BE893663946B5AABBDA57FFE4BA49973D547FA5DD
1236DEF9FA5A9CE52F7AF1947F42A6C3502A47E8EF7E8CEFDDD44D0BFE090EA3220C2B
52E11776DE36DD6C72D3B39A56F5D7295D26A69DB8CDAD1ABDBE1B21C1B754C9184E65
2CAE169E2F492FA0EE5908AC5CB3BE5F4C7F6CD9F41314D1BD9B1DE713A4E6C7DFB11D
2E64000ECFBBC89326B1322A8A227ECE7B919408C9187B5C9D53FC3985833E76D72164
40B7386569E4DE270C3616ABD2A91A657AC58AA872704CB3DD4DF08C77D03D8E3CEDB5
0D83BC3837FFE45D64B457AFE9A6ABF680637C51F80CB54691233BC4DE640026ACDAAC
3FC0749FA8353F6EAD5D362A63C1CF25ACA73A9CF3290B54C18DB3214AF078D918682E
513C434EFA06D9045571B1734CAE42990A1BE962D6E2A45846169EAAF2CDBD520813C2
D4DE97FFFABE582A02CA893F91EAF0EBCDCEB70B35850FDDF56EEA60C845A7E5C052C4
33344776E7A4C787690CC0E13F32373EE425CA10520C251D045C0AE73EE7A0CA83C858
E2E528CDBD117BC022ED3F5DDE40CED0128B761E29B11F422C8E7C4281BF94F6F75D07
0EE58426137548ABA38019A34DF1A66F700C29EF5545AC88BED75B5036801F0D8D4DB9
C6CCEA83D9DE3D626A04A80F218EFE9C74C173412A8A86786AB4A85403E8F8292CFED5
B8BA72FB5CE1BDD094AD9D633FD482F8FDBFA540DD2224149786ADA8DB6310A7C0C6E5
9167815B2CEF34E7C458C41B5C56A79414BA57073E9B06D28CA08C56ED5E685EEA2BA5
DD112F87B253A0D02AC7CF93EDE93F48A80B2DB57B254937EA80E9AC1CEBD36FD297EB
C8A3B42CBC3D2CA732891B49457F3F15AA3F9BF93553968A07CB1A834B392F27B2D152
47D93E46A6338694EA53CA0F5968109B4FAC9A#)(protected-at
"20210119T215925")))
Created: 20210119T215908

+ 23
- 0
org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key View File

@@ -0,0 +1,23 @@
Meta-Description: Example from GPG file 'keyformat.txt'.
Description: Key to sign all GnuPG released tarballs.
The key is actually stored on a smart card.
Use-for-ssh: yes
OpenSSH-cert: long base64 encoded string wrapped so that this
key file can be easily edited with a standard editor.
Token: D2760001240102000005000011730000 OPENPGP.1
Token: FF020001008A77C1 PIV.9C
Key: (shadowed-private-key
(rsa
(n #00AA1AD2A55FD8C8FDE9E1941772D9CC903FA43B268CB1B5A1BAFDC900
2961D8AEA153424DC851EF13B83AC64FBE365C59DC1BD3E83017C90D4365B4
83E02859FC13DB5842A00E969480DB96CE6F7D1C03600392B8E08EF0C01FC7
19F9F9086B25AD39B4F1C2A2DF3E2BE317110CFFF21D4A11455508FE407997
601260816C8422297C0637BB291C3A079B9CB38A92CE9E551F80AA0EBF4F0E
72C3F250461E4D31F23A7087857FC8438324A013634563D34EFDDCBF2EA80D
F9662C9CCD4BEF2522D8BDFED24CEF78DC6B309317407EAC576D889F88ADA0
8C4FFB480981FB68C5C6CA27503381D41018E6CDC52AAAE46B166BDC10637A
E186A02BA2497FDC5D1221#)
(e #00010001#)
(shadowed t1-v1
(#D2760001240102000005000011730000# OPENPGP.1)
)))

+ 155
- 0
org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal.keys;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Iterator;

import javax.crypto.Cipher;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class SecretKeysTest {

@BeforeClass
public static void ensureBC() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}

private static volatile Boolean haveOCB;

private static boolean ocbAvailable() {
Boolean haveIt = haveOCB;
if (haveIt != null) {
return haveIt.booleanValue();
}
try {
Cipher c = Cipher.getInstance("AES/OCB/NoPadding"); //$NON-NLS-1$
if (c == null) {
haveOCB = Boolean.FALSE;
return false;
}
} catch (NoClassDefFoundError | Exception e) {
haveOCB = Boolean.FALSE;
return false;
}
haveOCB = Boolean.TRUE;
return true;
}

private static class TestData {

final String name;

final boolean encrypted;

TestData(String name, boolean encrypted) {
this.name = name;
this.encrypted = encrypted;
}

@Override
public String toString() {
return name;
}
}

@Parameters(name = "{0}")
public static TestData[] initTestData() {
return new TestData[] {
new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false),
new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true),
new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true),
new TestData("faked", false) };
}

private static byte[] readTestKey(String filename) throws Exception {
try (InputStream in = new BufferedInputStream(
SecretKeysTest.class.getResourceAsStream(filename))) {
return SecretKeys.keyFromNameValueFormat(in);
}
}

private static PGPPublicKey readAsc(InputStream in)
throws IOException, PGPException {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());

Iterator<PGPPublicKeyRing> keyRings = pgpPub.getKeyRings();
while (keyRings.hasNext()) {
PGPPublicKeyRing keyRing = keyRings.next();

Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
if (keys.hasNext()) {
return keys.next();
}
}
return null;
}

// Injected by JUnit
@Parameter
public TestData data;

@Test
public void testKeyRead() throws Exception {
byte[] bytes = readTestKey(data.name + ".key");
assertEquals('(', bytes[0]);
assertEquals(')', bytes[bytes.length - 1]);
try (InputStream pubIn = this.getClass()
.getResourceAsStream(data.name + ".asc")) {
if (pubIn != null) {
PGPPublicKey publicKey = readAsc(pubIn);
// Do a full test trying to load the secret key.
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
try (InputStream in = new BufferedInputStream(this.getClass()
.getResourceAsStream(data.name + ".key"))) {
PGPSecretKey secretKey = SecretKeys.readSecretKey(in,
calculatorProvider, () -> "nonsense".toCharArray(),
publicKey);
assertNotNull(secretKey);
} catch (PGPException e) {
// Currently we may not be able to load OCB-encrypted keys.
assertTrue(e.getMessage().contains("OCB"));
assertTrue(data.encrypted);
assertFalse(ocbAvailable());
}
}
}
}

}

+ 3
- 5
org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF View File

@@ -18,6 +18,7 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.interfaces;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.util;version="[1.65.0,2.0.0)",
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.math.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.math.field;version="[1.65.0,2.0.0)",
@@ -25,14 +26,11 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.util;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
org.bouncycastle.util.io;version="[1.65.0,2.0.0)",
org.eclipse.jgit.annotations;version="[5.11.0,5.12.0)",
org.eclipse.jgit.api.errors;version="[5.11.0,5.12.0)",
org.eclipse.jgit.errors;version="[5.11.0,5.12.0)",
org.eclipse.jgit.lib;version="[5.11.0,5.12.0)",
org.eclipse.jgit.nls;version="[5.11.0,5.12.0)",
org.eclipse.jgit.transport;version="[5.11.0,5.12.0)",
org.eclipse.jgit.util;version="[5.11.0,5.12.0)",
org.slf4j;version="[1.7.0,2.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc;version="5.11.0",
org.eclipse.jgit.gpg.bc.internal;version="5.11.0";x-friends:="org.eclipse.jgit.gpg.bc.test",

+ 38
- 45
org.eclipse.jgit.gpg.bc/about.html View File

@@ -11,7 +11,7 @@
margin: 0.25in 0.5in 0.25in 0.5in;
tab-interval: 0.5in;
}
p {
p {
margin-left: auto;
margin-top: 0.5em;
margin-bottom: 0.5em;
@@ -36,60 +36,53 @@
<p>Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. </p>

<p>All rights reserved.</p>
<p>Redistribution and use in source and binary forms, with or without modification,
<p>Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
<ul><li>Redistributions of source code must retain the above copyright notice,
<ul><li>Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. </li>
<li>Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
<li>Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. </li>
<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its
contributors may be used to endorse or promote products derived from
<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission. </li></ul>
</p>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.</p>

<hr>
<p><b>SHA-1 UbcCheck - MIT</b></p>
<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>

<p>Copyright (c) 2017:</p>
<div class="ubc-name">
Marc Stevens
Cryptology Group
Centrum Wiskunde & Informatica
P.O. Box 94079, 1090 GB Amsterdam, Netherlands
marc@marc-stevens.nl
</div>
<div class="ubc-name">
Dan Shumow
Microsoft Research
danshu@microsoft.com
</div>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
<p>Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc.
(<a href="https://www.bouncycastle.org">https://www.bouncycastle.org</a>)</p>

<p>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
</p>
<p>
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</p>
<ul><li>The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.</li></ul>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</p>

</body>


+ 12
- 0
org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties View File

@@ -1,5 +1,7 @@
corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
cryptCipherError=Cannot create cipher to decrypt: {0}
cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
gpgNoCredentialsProvider=missing credentials provider
gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
@@ -7,10 +9,20 @@ gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
gpgNoSuchAlgorithm=Cannot decrypt encrypted secret key: encryption algorithm {0} is not available
gpgNotASigningKey=Secret key ({0}) is not suitable for signing
gpgKeyInfo=GPG Key (fingerprint {0})
gpgSigningCancelled=Signing was cancelled
nonSignatureError=Signature does not decode into a signature object
secretKeyTooShort=Secret key file corrupt; only {0} bytes read
sexprHexNotClosed=Hex number in s-expression not closed
sexprHexOdd=Hex number in s-expression has an odd number of digits
sexprStringInvalidEscape=Invalid escape {0} in s-expression
sexprStringInvalidEscapeAtEnd=Invalid s-expression: quoted string ends with escape character
sexprStringInvalidHexEscape=Invalid hex escape in s-expression
sexprStringInvalidOctalEscape=Invalid octal escape in s-expression
sexprStringNotClosed=String in s-expression not closed
sexprUnhandled=Unhandled token {0} in s-expression
signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
signatureKeyLookupError=Error occurred while looking for public key
signatureNoKeyInfo=No way to determine a public key from the signature

+ 12
- 0
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java View File

@@ -29,6 +29,8 @@ public final class BCText extends TranslationBundle {
// @formatter:off
/***/ public String corrupt25519Key;
/***/ public String credentialPassphrase;
/***/ public String cryptCipherError;
/***/ public String cryptWrongDecryptedLength;
/***/ public String gpgFailedToParseSecretKey;
/***/ public String gpgNoCredentialsProvider;
/***/ public String gpgNoKeygrip;
@@ -36,10 +38,20 @@ public final class BCText extends TranslationBundle {
/***/ public String gpgNoKeyInLegacySecring;
/***/ public String gpgNoPublicKeyFound;
/***/ public String gpgNoSecretKeyForPublicKey;
/***/ public String gpgNoSuchAlgorithm;
/***/ public String gpgNotASigningKey;
/***/ public String gpgKeyInfo;
/***/ public String gpgSigningCancelled;
/***/ public String nonSignatureError;
/***/ public String secretKeyTooShort;
/***/ public String sexprHexNotClosed;
/***/ public String sexprHexOdd;
/***/ public String sexprStringInvalidEscape;
/***/ public String sexprStringInvalidEscapeAtEnd;
/***/ public String sexprStringInvalidHexEscape;
/***/ public String sexprStringInvalidOctalEscape;
/***/ public String sexprStringNotClosed;
/***/ public String sexprUnhandled;
/***/ public String signatureInconsistent;
/***/ public String signatureKeyLookupError;
/***/ public String signatureNoKeyInfo;

+ 16
- 35
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java View File

@@ -30,7 +30,6 @@ import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;

import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.KeyBox;
@@ -48,16 +47,15 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
import org.eclipse.jgit.gpg.bc.internal.keys.SecretKeys;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.SystemReader;
@@ -77,17 +75,10 @@ public class BouncyCastleGpgKeyLocator {

}

/** Thrown if we try to read an encrypted private key without password. */
private static class EncryptedPgpKeyException extends RuntimeException {

private static final long serialVersionUID = 1L;

}

private static final Logger log = LoggerFactory
.getLogger(BouncyCastleGpgKeyLocator.class);

private static final Path GPG_DIRECTORY = findGpgDirectory();
static final Path GPG_DIRECTORY = findGpgDirectory();

private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
.resolve("pubring.kbx"); //$NON-NLS-1$
@@ -154,11 +145,13 @@ public class BouncyCastleGpgKeyLocator {

private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
PBEProtectionRemoverFactory passphraseProvider,
PGPPublicKey publicKey) throws IOException, PGPException {
SecretKeys.PassphraseSupplier passphraseSupplier,
PGPPublicKey publicKey)
throws IOException, PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException {
try (InputStream in = newInputStream(keyFile)) {
return new SExprParser(calculatorProvider).parseSecretKey(
new BufferedInputStream(in), passphraseProvider, publicKey);
return SecretKeys.readSecretKey(in, calculatorProvider,
passphraseSupplier, publicKey);
}
}

@@ -483,29 +476,17 @@ public class BouncyCastleGpgKeyLocator {
try {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
PBEProtectionRemoverFactory passphraseProvider = p -> {
throw new EncryptedPgpKeyException();
};
clearPrompt = true;
PGPSecretKey secretKey = null;
try {
// Try without passphrase
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (EncryptedPgpKeyException e) {
// Let's try again with a passphrase
passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath));
clearPrompt = true;
try {
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (PGPException e1) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);

}
() -> passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath),
publicKey);
} catch (PGPException e) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
}
if (secretKey != null) {
if (!secretKey.isSigningKey()) {

+ 3
- 4
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java View File

@@ -17,8 +17,8 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem.CharArrayType;
import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
import org.eclipse.jgit.transport.CredentialItem.Password;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;

@@ -31,7 +31,7 @@ import org.eclipse.jgit.transport.URIish;
*/
class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {

private CharArrayType passphrase;
private Password passphrase;

private CredentialsProvider credentialsProvider;

@@ -78,8 +78,7 @@ class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
if (passphrase == null) {
passphrase = new CharArrayType(BCText.get().credentialPassphrase,
true);
passphrase = new Password(BCText.get().credentialPassphrase);
}

if (credentialsProvider == null) {

+ 6
- 4
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java View File

@@ -49,7 +49,7 @@ import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.util.StringUtils;

/**
* GPG Signer using BouncyCastle library
* GPG Signer using the BouncyCastle library.
*/
public class BouncyCastleGpgSigner extends GpgSigner
implements GpgObjectSigner {
@@ -97,8 +97,9 @@ public class BouncyCastleGpgSigner extends GpgSigner
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
return gpgKey != null;
} catch (PGPException | IOException | NoSuchAlgorithmException
| NoSuchProviderException | URISyntaxException e) {
} catch (CanceledException e) {
throw e;
} catch (Exception e) {
return false;
}
}
@@ -143,7 +144,8 @@ public class BouncyCastleGpgSigner extends GpgSigner
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
committer,
passphrasePrompt);
PGPSecretKey secretKey = gpgKey.getSecretKey();
if (secretKey == null) {
throw new JGitInternalException(

+ 121
- 0
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal.keys;

import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.util.Arrays;
import org.eclipse.jgit.gpg.bc.internal.BCText;

/**
* A {@link PBEProtectionRemoverFactory} using AES/OCB/NoPadding for decryption.
* It accepts an AAD in the factory's constructor, so the factory can be used to
* create a {@link PBESecretKeyDecryptor} only for a particular input.
* <p>
* For JGit's needs, this is sufficient, but for a general upstream
* implementation that limitation might not be acceptable.
* </p>
*/
class OCBPBEProtectionRemoverFactory
implements PBEProtectionRemoverFactory {

private final PGPDigestCalculatorProvider calculatorProvider;

private final char[] passphrase;

private final byte[] aad;

/**
* Creates a new factory instance with the given parameters.
* <p>
* Because the AAD is given at factory level, the {@link PBESecretKeyDecryptor}s
* created by the factory can be used to decrypt only a particular input
* matching this AAD.
* </p>
*
* @param passphrase to use for secret key derivation
* @param calculatorProvider for computing digests
* @param aad for the OCB decryption
*/
OCBPBEProtectionRemoverFactory(char[] passphrase,
PGPDigestCalculatorProvider calculatorProvider, byte[] aad) {
this.calculatorProvider = calculatorProvider;
this.passphrase = passphrase;
this.aad = aad;
}

@Override
public PBESecretKeyDecryptor createDecryptor(String protection)
throws PGPException {
return new PBESecretKeyDecryptor(passphrase, calculatorProvider) {

@Override
public byte[] recoverKeyData(int encAlgorithm, byte[] key,
byte[] iv, byte[] encrypted, int encryptedOffset,
int encryptedLength) throws PGPException {
String algorithmName = PGPUtil
.getSymmetricCipherName(encAlgorithm);
byte[] decrypted = null;
try {
Cipher c = Cipher
.getInstance(algorithmName + "/OCB/NoPadding"); //$NON-NLS-1$
SecretKey secretKey = new SecretKeySpec(key, algorithmName);
c.init(Cipher.DECRYPT_MODE, secretKey,
new IvParameterSpec(iv));
c.updateAAD(aad);
decrypted = new byte[c.getOutputSize(encryptedLength)];
int decryptedLength = c.update(encrypted, encryptedOffset,
encryptedLength, decrypted);
// doFinal() for OCB will check the MAC and throw an
// exception if it doesn't match
decryptedLength += c.doFinal(decrypted, decryptedLength);
if (decryptedLength != decrypted.length) {
throw new PGPException(MessageFormat.format(
BCText.get().cryptWrongDecryptedLength,
Integer.valueOf(decryptedLength),
Integer.valueOf(decrypted.length)));
}
byte[] result = decrypted;
decrypted = null; // Don't clear in finally
return result;
} catch (NoClassDefFoundError e) {
String msg = MessageFormat.format(
BCText.get().gpgNoSuchAlgorithm,
algorithmName + "/OCB"); //$NON-NLS-1$
throw new PGPException(msg,
new NoSuchAlgorithmException(msg, e));
} catch (PGPException e) {
throw e;
} catch (Exception e) {
throw new PGPException(
MessageFormat.format(BCText.get().cryptCipherError,
e.getLocalizedMessage()),
e);
} finally {
if (decrypted != null) {
// Prevent halfway decrypted data leaking.
Arrays.fill(decrypted, (byte) 0);
}
}
}
};
}
}

+ 826
- 0
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java View File

@@ -0,0 +1,826 @@
/*
* Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
*including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
* </p>
* <p>
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
* </p>
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
* </p>
*/
package org.eclipse.jgit.gpg.bc.internal.keys;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Date;

import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.bcpg.DSAPublicBCPGKey;
import org.bouncycastle.bcpg.DSASecretBCPGKey;
import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
import org.bouncycastle.bcpg.ECPublicBCPGKey;
import org.bouncycastle.bcpg.ECSecretBCPGKey;
import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.bcpg.RSAPublicBCPGKey;
import org.bouncycastle.bcpg.RSASecretBCPGKey;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.SecretKeyPacket;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Strings;

/**
* A parser for secret keys stored in s-expressions. Original BouncyCastle code
* modified by the JGit team to:
* <ul>
* <li>handle unencrypted DSA, EC, and ElGamal keys (upstream only handles
* unencrypted RSA), and</li>
* <li>handle secret keys using AES/OCB as encryption (those don't have a
* hash).</li>
* </ul>
*/
@SuppressWarnings("nls")
public class SExprParser {
private final PGPDigestCalculatorProvider digestProvider;

/**
* Base constructor.
*
* @param digestProvider
* a provider for digest calculations. Used to confirm key
* protection hashes.
*/
public SExprParser(PGPDigestCalculatorProvider digestProvider) {
this.digestProvider = digestProvider;
}

/**
* Parse a secret key from one of the GPG S expression keys associating it
* with the passed in public key.
*
* @param inputStream
* to read from
* @param keyProtectionRemoverFactory
* for decrypting encrypted keys
* @param pubKey
* the private key should belong to
*
* @return a secret key object.
* @throws IOException
* @throws PGPException
*/
public PGPSecretKey parseSecretKey(InputStream inputStream,
PBEProtectionRemoverFactory keyProtectionRemoverFactory,
PGPPublicKey pubKey) throws IOException, PGPException {
SXprUtils.skipOpenParenthesis(inputStream);

String type;

type = SXprUtils.readString(inputStream, inputStream.read());
if (type.equals("protected-private-key")
|| type.equals("private-key")) {
SXprUtils.skipOpenParenthesis(inputStream);

String keyType = SXprUtils.readString(inputStream,
inputStream.read());
if (keyType.equals("ecc")) {
SXprUtils.skipOpenParenthesis(inputStream);

String curveID = SXprUtils.readString(inputStream,
inputStream.read());
String curveName = SXprUtils.readString(inputStream,
inputStream.read());

SXprUtils.skipCloseParenthesis(inputStream);

byte[] qVal;

SXprUtils.skipOpenParenthesis(inputStream);

type = SXprUtils.readString(inputStream, inputStream.read());
if (type.equals("q")) {
qVal = SXprUtils.readBytes(inputStream, inputStream.read());
} else {
throw new PGPException("no q value found");
}

SXprUtils.skipCloseParenthesis(inputStream);

BigInteger d = processECSecretKey(inputStream, curveID,
curveName, qVal, keyProtectionRemoverFactory);

if (curveName.startsWith("NIST ")) {
curveName = curveName.substring("NIST ".length());
}

ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
ECNamedCurveTable.getOID(curveName),
new BigInteger(1, qVal));
ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
.getPublicKeyPacket().getKey();
if (!basePubKey.getCurveOID().equals(assocPubKey.getCurveOID())
|| !basePubKey.getEncodedPoint()
.equals(assocPubKey.getEncodedPoint())) {
throw new PGPException(
"passed in public key does not match secret key");
}

return new PGPSecretKey(
new SecretKeyPacket(pubKey.getPublicKeyPacket(),
SymmetricKeyAlgorithmTags.NULL, null, null,
new ECSecretBCPGKey(d).getEncoded()),
pubKey);
} else if (keyType.equals("dsa")) {
BigInteger p = readBigInteger("p", inputStream);
BigInteger q = readBigInteger("q", inputStream);
BigInteger g = readBigInteger("g", inputStream);

BigInteger y = readBigInteger("y", inputStream);

BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
keyProtectionRemoverFactory);

DSAPublicBCPGKey basePubKey = new DSAPublicBCPGKey(p, q, g, y);
DSAPublicBCPGKey assocPubKey = (DSAPublicBCPGKey) pubKey
.getPublicKeyPacket().getKey();
if (!basePubKey.getP().equals(assocPubKey.getP())
|| !basePubKey.getQ().equals(assocPubKey.getQ())
|| !basePubKey.getG().equals(assocPubKey.getG())
|| !basePubKey.getY().equals(assocPubKey.getY())) {
throw new PGPException(
"passed in public key does not match secret key");
}
return new PGPSecretKey(
new SecretKeyPacket(pubKey.getPublicKeyPacket(),
SymmetricKeyAlgorithmTags.NULL, null, null,
new DSASecretBCPGKey(x).getEncoded()),
pubKey);
} else if (keyType.equals("elg")) {
BigInteger p = readBigInteger("p", inputStream);
BigInteger g = readBigInteger("g", inputStream);

BigInteger y = readBigInteger("y", inputStream);

BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
keyProtectionRemoverFactory);

ElGamalPublicBCPGKey basePubKey = new ElGamalPublicBCPGKey(p, g,
y);
ElGamalPublicBCPGKey assocPubKey = (ElGamalPublicBCPGKey) pubKey
.getPublicKeyPacket().getKey();
if (!basePubKey.getP().equals(assocPubKey.getP())
|| !basePubKey.getG().equals(assocPubKey.getG())
|| !basePubKey.getY().equals(assocPubKey.getY())) {
throw new PGPException(
"passed in public key does not match secret key");
}

return new PGPSecretKey(
new SecretKeyPacket(pubKey.getPublicKeyPacket(),
SymmetricKeyAlgorithmTags.NULL, null, null,
new ElGamalSecretBCPGKey(x).getEncoded()),
pubKey);
} else if (keyType.equals("rsa")) {
BigInteger n = readBigInteger("n", inputStream);
BigInteger e = readBigInteger("e", inputStream);

BigInteger[] values = processRSASecretKey(inputStream, n, e,
keyProtectionRemoverFactory);

// TODO: type of RSA key?
RSAPublicBCPGKey basePubKey = new RSAPublicBCPGKey(n, e);
RSAPublicBCPGKey assocPubKey = (RSAPublicBCPGKey) pubKey
.getPublicKeyPacket().getKey();
if (!basePubKey.getModulus().equals(assocPubKey.getModulus())
|| !basePubKey.getPublicExponent()
.equals(assocPubKey.getPublicExponent())) {
throw new PGPException(
"passed in public key does not match secret key");
}

return new PGPSecretKey(new SecretKeyPacket(
pubKey.getPublicKeyPacket(),
SymmetricKeyAlgorithmTags.NULL, null, null,
new RSASecretBCPGKey(values[0], values[1], values[2])
.getEncoded()),
pubKey);
} else {
throw new PGPException("unknown key type: " + keyType);
}
}

throw new PGPException("unknown key type found");
}

/**
* Parse a secret key from one of the GPG S expression keys.
*
* @param inputStream
* to read from
* @param keyProtectionRemoverFactory
* for decrypting encrypted keys
* @param fingerPrintCalculator
* for calculating key fingerprints
*
* @return a secret key object.
* @throws IOException
* @throws PGPException
*/
public PGPSecretKey parseSecretKey(InputStream inputStream,
PBEProtectionRemoverFactory keyProtectionRemoverFactory,
KeyFingerPrintCalculator fingerPrintCalculator)
throws IOException, PGPException {
SXprUtils.skipOpenParenthesis(inputStream);

String type;

type = SXprUtils.readString(inputStream, inputStream.read());
if (type.equals("protected-private-key")
|| type.equals("private-key")) {
SXprUtils.skipOpenParenthesis(inputStream);

String keyType = SXprUtils.readString(inputStream,
inputStream.read());
if (keyType.equals("ecc")) {
SXprUtils.skipOpenParenthesis(inputStream);

String curveID = SXprUtils.readString(inputStream,
inputStream.read());
String curveName = SXprUtils.readString(inputStream,
inputStream.read());

if (curveName.startsWith("NIST ")) {
curveName = curveName.substring("NIST ".length());
}

SXprUtils.skipCloseParenthesis(inputStream);

byte[] qVal;

SXprUtils.skipOpenParenthesis(inputStream);

type = SXprUtils.readString(inputStream, inputStream.read());
if (type.equals("q")) {
qVal = SXprUtils.readBytes(inputStream, inputStream.read());
} else {
throw new PGPException("no q value found");
}

PublicKeyPacket pubPacket = new PublicKeyPacket(
PublicKeyAlgorithmTags.ECDSA, new Date(),
new ECDSAPublicBCPGKey(
ECNamedCurveTable.getOID(curveName),
new BigInteger(1, qVal)));

SXprUtils.skipCloseParenthesis(inputStream);

BigInteger d = processECSecretKey(inputStream, curveID,
curveName, qVal, keyProtectionRemoverFactory);

return new PGPSecretKey(
new SecretKeyPacket(pubPacket,
SymmetricKeyAlgorithmTags.NULL, null, null,
new ECSecretBCPGKey(d).getEncoded()),
new PGPPublicKey(pubPacket, fingerPrintCalculator));
} else if (keyType.equals("dsa")) {
BigInteger p = readBigInteger("p", inputStream);
BigInteger q = readBigInteger("q", inputStream);
BigInteger g = readBigInteger("g", inputStream);

BigInteger y = readBigInteger("y", inputStream);

BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
keyProtectionRemoverFactory);

PublicKeyPacket pubPacket = new PublicKeyPacket(
PublicKeyAlgorithmTags.DSA, new Date(),
new DSAPublicBCPGKey(p, q, g, y));

return new PGPSecretKey(
new SecretKeyPacket(pubPacket,
SymmetricKeyAlgorithmTags.NULL, null, null,
new DSASecretBCPGKey(x).getEncoded()),
new PGPPublicKey(pubPacket, fingerPrintCalculator));
} else if (keyType.equals("elg")) {
BigInteger p = readBigInteger("p", inputStream);
BigInteger g = readBigInteger("g", inputStream);

BigInteger y = readBigInteger("y", inputStream);

BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
keyProtectionRemoverFactory);

PublicKeyPacket pubPacket = new PublicKeyPacket(
PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, new Date(),
new ElGamalPublicBCPGKey(p, g, y));

return new PGPSecretKey(
new SecretKeyPacket(pubPacket,
SymmetricKeyAlgorithmTags.NULL, null, null,
new ElGamalSecretBCPGKey(x).getEncoded()),
new PGPPublicKey(pubPacket, fingerPrintCalculator));
} else if (keyType.equals("rsa")) {
BigInteger n = readBigInteger("n", inputStream);
BigInteger e = readBigInteger("e", inputStream);

BigInteger[] values = processRSASecretKey(inputStream, n, e,
keyProtectionRemoverFactory);

// TODO: type of RSA key?
PublicKeyPacket pubPacket = new PublicKeyPacket(
PublicKeyAlgorithmTags.RSA_GENERAL, new Date(),
new RSAPublicBCPGKey(n, e));

return new PGPSecretKey(
new SecretKeyPacket(pubPacket,
SymmetricKeyAlgorithmTags.NULL, null, null,
new RSASecretBCPGKey(values[0], values[1],
values[2]).getEncoded()),
new PGPPublicKey(pubPacket, fingerPrintCalculator));
} else {
throw new PGPException("unknown key type: " + keyType);
}
}

throw new PGPException("unknown key type found");
}

private BigInteger readBigInteger(String expectedType,
InputStream inputStream) throws IOException, PGPException {
SXprUtils.skipOpenParenthesis(inputStream);

String type = SXprUtils.readString(inputStream, inputStream.read());
if (!type.equals(expectedType)) {
throw new PGPException(expectedType + " value expected");
}

byte[] nBytes = SXprUtils.readBytes(inputStream, inputStream.read());
BigInteger v = new BigInteger(1, nBytes);

SXprUtils.skipCloseParenthesis(inputStream);

return v;
}

private static byte[][] extractData(InputStream inputStream,
PBEProtectionRemoverFactory keyProtectionRemoverFactory)
throws PGPException, IOException {
byte[] data;
byte[] protectedAt = null;

SXprUtils.skipOpenParenthesis(inputStream);

String type = SXprUtils.readString(inputStream, inputStream.read());
if (type.equals("protected")) {
String protection = SXprUtils.readString(inputStream,
inputStream.read());

SXprUtils.skipOpenParenthesis(inputStream);

S2K s2k = SXprUtils.parseS2K(inputStream);

byte[] iv = SXprUtils.readBytes(inputStream, inputStream.read());

SXprUtils.skipCloseParenthesis(inputStream);

byte[] secKeyData = SXprUtils.readBytes(inputStream,
inputStream.read());

SXprUtils.skipCloseParenthesis(inputStream);

PBESecretKeyDecryptor keyDecryptor = keyProtectionRemoverFactory
.createDecryptor(protection);

// TODO: recognise other algorithms
byte[] key = keyDecryptor.makeKeyFromPassPhrase(
SymmetricKeyAlgorithmTags.AES_128, s2k);

data = keyDecryptor.recoverKeyData(
SymmetricKeyAlgorithmTags.AES_128, key, iv, secKeyData, 0,
secKeyData.length);

// check if protected at is present
if (inputStream.read() == '(') {
ByteArrayOutputStream bOut = new ByteArrayOutputStream();

bOut.write('(');
int ch;
while ((ch = inputStream.read()) >= 0 && ch != ')') {
bOut.write(ch);
}

if (ch != ')') {
throw new IOException("unexpected end to SExpr");
}

bOut.write(')');

protectedAt = bOut.toByteArray();
}

SXprUtils.skipCloseParenthesis(inputStream);
SXprUtils.skipCloseParenthesis(inputStream);
} else if (type.equals("d") || type.equals("x")) {
// JGit modification: unencrypted DSA or ECC keys can have an "x"
// here
return null;
} else {
throw new PGPException("protected block not found");
}

return new byte[][] { data, protectedAt };
}

private BigInteger processDSASecretKey(InputStream inputStream,
BigInteger p, BigInteger q, BigInteger g, BigInteger y,
PBEProtectionRemoverFactory keyProtectionRemoverFactory)
throws IOException, PGPException {
String type;
byte[][] basicData = extractData(inputStream,
keyProtectionRemoverFactory);

// JGit modification: handle unencrypted DSA keys
if (basicData == null) {
byte[] nBytes = SXprUtils.readBytes(inputStream,
inputStream.read());
BigInteger x = new BigInteger(1, nBytes);
SXprUtils.skipCloseParenthesis(inputStream);
return x;
}

byte[] keyData = basicData[0];
byte[] protectedAt = basicData[1];

//
// parse the secret key S-expr
//
InputStream keyIn = new ByteArrayInputStream(keyData);

SXprUtils.skipOpenParenthesis(keyIn);
SXprUtils.skipOpenParenthesis(keyIn);

BigInteger x = readBigInteger("x", keyIn);

SXprUtils.skipCloseParenthesis(keyIn);

// JGit modification: OCB-encrypted keys don't have and don't need a
// hash
if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
return x;
}

SXprUtils.skipOpenParenthesis(keyIn);
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("hash")) {
throw new PGPException("hash keyword expected");
}
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("sha1")) {
throw new PGPException("hash keyword expected");
}

byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

SXprUtils.skipCloseParenthesis(keyIn);

if (digestProvider != null) {
PGPDigestCalculator digestCalculator = digestProvider
.get(HashAlgorithmTags.SHA1);

OutputStream dOut = digestCalculator.getOutputStream();

dOut.write(Strings.toByteArray("(3:dsa"));
writeCanonical(dOut, "p", p);
writeCanonical(dOut, "q", q);
writeCanonical(dOut, "g", g);
writeCanonical(dOut, "y", y);
writeCanonical(dOut, "x", x);

// check protected-at
if (protectedAt != null) {
dOut.write(protectedAt);
}

dOut.write(Strings.toByteArray(")"));

byte[] check = digestCalculator.getDigest();
if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
throw new PGPException(
"checksum on protected data failed in SExpr");
}
}

return x;
}

private BigInteger processElGamalSecretKey(InputStream inputStream,
BigInteger p, BigInteger g, BigInteger y,
PBEProtectionRemoverFactory keyProtectionRemoverFactory)
throws IOException, PGPException {
String type;
byte[][] basicData = extractData(inputStream,
keyProtectionRemoverFactory);

// JGit modification: handle unencrypted EC keys
if (basicData == null) {
byte[] nBytes = SXprUtils.readBytes(inputStream,
inputStream.read());
BigInteger x = new BigInteger(1, nBytes);
SXprUtils.skipCloseParenthesis(inputStream);
return x;
}

byte[] keyData = basicData[0];
byte[] protectedAt = basicData[1];

//
// parse the secret key S-expr
//
InputStream keyIn = new ByteArrayInputStream(keyData);

SXprUtils.skipOpenParenthesis(keyIn);
SXprUtils.skipOpenParenthesis(keyIn);

BigInteger x = readBigInteger("x", keyIn);

SXprUtils.skipCloseParenthesis(keyIn);

// JGit modification: OCB-encrypted keys don't have and don't need a
// hash
if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
return x;
}

SXprUtils.skipOpenParenthesis(keyIn);
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("hash")) {
throw new PGPException("hash keyword expected");
}
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("sha1")) {
throw new PGPException("hash keyword expected");
}

byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

SXprUtils.skipCloseParenthesis(keyIn);

if (digestProvider != null) {
PGPDigestCalculator digestCalculator = digestProvider
.get(HashAlgorithmTags.SHA1);

OutputStream dOut = digestCalculator.getOutputStream();

dOut.write(Strings.toByteArray("(3:elg"));
writeCanonical(dOut, "p", p);
writeCanonical(dOut, "g", g);
writeCanonical(dOut, "y", y);
writeCanonical(dOut, "x", x);

// check protected-at
if (protectedAt != null) {
dOut.write(protectedAt);
}

dOut.write(Strings.toByteArray(")"));

byte[] check = digestCalculator.getDigest();
if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
throw new PGPException(
"checksum on protected data failed in SExpr");
}
}

return x;
}

private BigInteger processECSecretKey(InputStream inputStream,
String curveID, String curveName, byte[] qVal,
PBEProtectionRemoverFactory keyProtectionRemoverFactory)
throws IOException, PGPException {
String type;

byte[][] basicData = extractData(inputStream,
keyProtectionRemoverFactory);

// JGit modification: handle unencrypted EC keys
if (basicData == null) {
byte[] nBytes = SXprUtils.readBytes(inputStream,
inputStream.read());
BigInteger d = new BigInteger(1, nBytes);
SXprUtils.skipCloseParenthesis(inputStream);
return d;
}

byte[] keyData = basicData[0];
byte[] protectedAt = basicData[1];

//
// parse the secret key S-expr
//
InputStream keyIn = new ByteArrayInputStream(keyData);

SXprUtils.skipOpenParenthesis(keyIn);
SXprUtils.skipOpenParenthesis(keyIn);
BigInteger d = readBigInteger("d", keyIn);
SXprUtils.skipCloseParenthesis(keyIn);

// JGit modification: OCB-encrypted keys don't have and don't need a
// hash
if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
return d;
}

SXprUtils.skipOpenParenthesis(keyIn);

type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("hash")) {
throw new PGPException("hash keyword expected");
}
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("sha1")) {
throw new PGPException("hash keyword expected");
}

byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

SXprUtils.skipCloseParenthesis(keyIn);

if (digestProvider != null) {
PGPDigestCalculator digestCalculator = digestProvider
.get(HashAlgorithmTags.SHA1);

OutputStream dOut = digestCalculator.getOutputStream();

dOut.write(Strings.toByteArray("(3:ecc"));

dOut.write(Strings.toByteArray("(" + curveID.length() + ":"
+ curveID + curveName.length() + ":" + curveName + ")"));

writeCanonical(dOut, "q", qVal);
writeCanonical(dOut, "d", d);

// check protected-at
if (protectedAt != null) {
dOut.write(protectedAt);
}

dOut.write(Strings.toByteArray(")"));

byte[] check = digestCalculator.getDigest();

if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
throw new PGPException(
"checksum on protected data failed in SExpr");
}
}

return d;
}

private BigInteger[] processRSASecretKey(InputStream inputStream,
BigInteger n, BigInteger e,
PBEProtectionRemoverFactory keyProtectionRemoverFactory)
throws IOException, PGPException {
String type;
byte[][] basicData = extractData(inputStream,
keyProtectionRemoverFactory);

byte[] keyData;
byte[] protectedAt = null;

InputStream keyIn;
BigInteger d;

if (basicData == null) {
keyIn = inputStream;
byte[] nBytes = SXprUtils.readBytes(inputStream,
inputStream.read());
d = new BigInteger(1, nBytes);

SXprUtils.skipCloseParenthesis(inputStream);

} else {
keyData = basicData[0];
protectedAt = basicData[1];

keyIn = new ByteArrayInputStream(keyData);

SXprUtils.skipOpenParenthesis(keyIn);
SXprUtils.skipOpenParenthesis(keyIn);
d = readBigInteger("d", keyIn);
}

//
// parse the secret key S-expr
//

BigInteger p = readBigInteger("p", keyIn);
BigInteger q = readBigInteger("q", keyIn);
BigInteger u = readBigInteger("u", keyIn);

// JGit modification: OCB-encrypted keys don't have and don't need a
// hash
if (basicData == null
|| keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
return new BigInteger[] { d, p, q, u };
}

SXprUtils.skipCloseParenthesis(keyIn);

SXprUtils.skipOpenParenthesis(keyIn);
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("hash")) {
throw new PGPException("hash keyword expected");
}
type = SXprUtils.readString(keyIn, keyIn.read());

if (!type.equals("sha1")) {
throw new PGPException("hash keyword expected");
}

byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

SXprUtils.skipCloseParenthesis(keyIn);

if (digestProvider != null) {
PGPDigestCalculator digestCalculator = digestProvider
.get(HashAlgorithmTags.SHA1);

OutputStream dOut = digestCalculator.getOutputStream();

dOut.write(Strings.toByteArray("(3:rsa"));

writeCanonical(dOut, "n", n);
writeCanonical(dOut, "e", e);
writeCanonical(dOut, "d", d);
writeCanonical(dOut, "p", p);
writeCanonical(dOut, "q", q);
writeCanonical(dOut, "u", u);

// check protected-at
if (protectedAt != null) {
dOut.write(protectedAt);
}

dOut.write(Strings.toByteArray(")"));

byte[] check = digestCalculator.getDigest();

if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
throw new PGPException(
"checksum on protected data failed in SExpr");
}
}

return new BigInteger[] { d, p, q, u };
}

private void writeCanonical(OutputStream dOut, String label, BigInteger i)
throws IOException {
writeCanonical(dOut, label, i.toByteArray());
}

private void writeCanonical(OutputStream dOut, String label, byte[] data)
throws IOException {
dOut.write(Strings.toByteArray(
"(" + label.length() + ":" + label + data.length + ":"));
dOut.write(data);
dOut.write(Strings.toByteArray(")"));
}
}

+ 110
- 0
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
*including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
* </p>
* <p>
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
* </p>
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
* </p>
*/
package org.eclipse.jgit.gpg.bc.internal.keys;

// This class is an unmodified copy from Bouncy Castle; needed because it's package-visible only and used by SExprParser.

import java.io.IOException;
import java.io.InputStream;

import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.util.io.Streams;

/**
* Utility functions for looking a S-expression keys. This class will move when
* it finds a better home!
* <p>
* Format documented here:
* http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/keyformat.txt;h=42c4b1f06faf1bbe71ffadc2fee0fad6bec91a97;hb=refs/heads/master
* </p>
*/
class SXprUtils {
private static int readLength(InputStream in, int ch) throws IOException {
int len = ch - '0';

while ((ch = in.read()) >= 0 && ch != ':') {
len = len * 10 + ch - '0';
}

return len;
}

static String readString(InputStream in, int ch) throws IOException {
int len = readLength(in, ch);

char[] chars = new char[len];

for (int i = 0; i != chars.length; i++) {
chars[i] = (char) in.read();
}

return new String(chars);
}

static byte[] readBytes(InputStream in, int ch) throws IOException {
int len = readLength(in, ch);

byte[] data = new byte[len];

Streams.readFully(in, data);

return data;
}

static S2K parseS2K(InputStream in) throws IOException {
skipOpenParenthesis(in);

// Algorithm is hard-coded to SHA1 below anyway.
readString(in, in.read());
byte[] iv = readBytes(in, in.read());
final long iterationCount = Long.parseLong(readString(in, in.read()));

skipCloseParenthesis(in);

// we have to return the actual iteration count provided.
S2K s2k = new S2K(HashAlgorithmTags.SHA1, iv, (int) iterationCount) {
@Override
public long getIterationCount() {
return iterationCount;
}
};

return s2k;
}

static void skipOpenParenthesis(InputStream in) throws IOException {
int ch = in.read();
if (ch != '(') {
throw new IOException(
"unknown character encountered: " + (char) ch); //$NON-NLS-1$
}
}

static void skipCloseParenthesis(InputStream in) throws IOException {
int ch = in.read();
if (ch != ')') {
throw new IOException("unknown character encountered"); //$NON-NLS-1$
}
}
}

+ 597
- 0
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java View File

@@ -0,0 +1,597 @@
/*
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal.keys;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.io.Streams;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.BCText;
import org.eclipse.jgit.util.RawParseUtils;

/**
* Utilities for reading GPG secret keys from a gpg-agent key file.
*/
public final class SecretKeys {

private SecretKeys() {
// No instantiation.
}

/**
* Something that can supply a passphrase to decrypt an encrypted secret
* key.
*/
public interface PassphraseSupplier {

/**
* Supplies a passphrase.
*
* @return the passphrase
* @throws PGPException
* if no passphrase can be obtained
* @throws CanceledException
* if the user canceled passphrase entry
* @throws UnsupportedCredentialItem
* if an internal error occurred
* @throws URISyntaxException
* if an internal error occurred
*/
char[] getPassphrase() throws PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException;
}

private static final byte[] PROTECTED_KEY = "protected-private-key" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);

private static final byte[] OCB_PROTECTED = "openpgp-s2k3-ocb-aes" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);

/**
* Reads a GPG secret key from the given stream.
*
* @param in
* {@link InputStream} to read from, doesn't need to be buffered
* @param calculatorProvider
* for checking digests
* @param passphraseSupplier
* for decrypting encrypted keys
* @param publicKey
* the secret key should be for
* @return the secret key
* @throws IOException
* if the stream cannot be parsed
* @throws PGPException
* if thrown by the underlying S-Expression parser, for instance
* when the passphrase is wrong
* @throws CanceledException
* if thrown by the {@code passphraseSupplier}
* @throws UnsupportedCredentialItem
* if thrown by the {@code passphraseSupplier}
* @throws URISyntaxException
* if thrown by the {@code passphraseSupplier}
*/
public static PGPSecretKey readSecretKey(InputStream in,
PGPDigestCalculatorProvider calculatorProvider,
PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey)
throws IOException, PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException {
byte[] data = Streams.readAll(in);
if (data.length == 0) {
throw new EOFException();
} else if (data.length < 4 + PROTECTED_KEY.length) {
// +4 for "(21:" for a binary protected key
throw new IOException(
MessageFormat.format(BCText.get().secretKeyTooShort,
Integer.toUnsignedString(data.length)));
}
SExprParser parser = new SExprParser(calculatorProvider);
byte firstChar = data[0];
try {
if (firstChar == '(') {
// Binary format.
if (!matches(data, 4, PROTECTED_KEY)) {
// Not encrypted binary format.
return parser.parseSecretKey(in, null, publicKey);
}
// AES/CBC encrypted.
PBEProtectionRemoverFactory decryptor = new JcePBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(), calculatorProvider);
try (InputStream sIn = new ByteArrayInputStream(data)) {
return parser.parseSecretKey(sIn, decryptor, publicKey);
}
}
// Assume it's the new key-value format.
try (ByteArrayInputStream keyIn = new ByteArrayInputStream(data)) {
byte[] rawData = keyFromNameValueFormat(keyIn);
if (!matches(rawData, 1, PROTECTED_KEY)) {
// Not encrypted human-readable format.
try (InputStream sIn = new ByteArrayInputStream(
convertSexpression(rawData))) {
return parser.parseSecretKey(sIn, null, publicKey);
}
}
// An encrypted key from a key-value file. Most likely AES/OCB
// encrypted.
boolean isOCB[] = { false };
byte[] sExp = convertSexpression(rawData, isOCB);
PBEProtectionRemoverFactory decryptor;
if (isOCB[0]) {
decryptor = new OCBPBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(),
calculatorProvider, getAad(sExp));
} else {
decryptor = new JcePBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(),
calculatorProvider);
}
try (InputStream sIn = new ByteArrayInputStream(sExp)) {
return parser.parseSecretKey(sIn, decryptor, publicKey);
}
}
} catch (IOException e) {
throw new PGPException(e.getLocalizedMessage(), e);
}
}

/**
* Extract the AAD for the OCB decryption from an s-expression.
*
* @param sExp
* buffer containing a valid binary s-expression
* @return the AAD
*/
private static byte[] getAad(byte[] sExp) {
// Given a key
// @formatter:off
// (protected-private-key (rsa ... (protected openpgp-s2k3-ocb-aes ... )(protected-at ...)))
// A B C D
// The AAD is [A..B)[C..D). (From the binary serialized form.)
// @formatter:on
int i = 1; // Skip initial '('
while (sExp[i] != '(') {
i++;
}
int aadStart = i++;
int aadEnd = skip(sExp, aadStart);
byte[] protectedPrefix = "(9:protected" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);
while (!matches(sExp, i, protectedPrefix)) {
i++;
}
int protectedStart = i;
int protectedEnd = skip(sExp, protectedStart);
byte[] aadData = new byte[aadEnd - aadStart
- (protectedEnd - protectedStart)];
System.arraycopy(sExp, aadStart, aadData, 0, protectedStart - aadStart);
System.arraycopy(sExp, protectedEnd, aadData, protectedStart - aadStart,
aadEnd - protectedEnd);
return aadData;
}

/**
* Skips a list including nested lists.
*
* @param sExp
* buffer containing valid binary s-expression data
* @param start
* index of the opening '(' of the list to skip
* @return the index after the closing ')' of the skipped list
*/
private static int skip(byte[] sExp, int start) {
int i = start + 1;
int depth = 1;
while (depth > 0) {
switch (sExp[i]) {
case '(':
depth++;
break;
case ')':
depth--;
break;
default:
// We must be on a length
int j = i;
while (sExp[j] >= '0' && sExp[j] <= '9') {
j++;
}
// j is on the colon
int length = Integer.parseInt(
new String(sExp, i, j - i, StandardCharsets.US_ASCII));
i = j + length;
}
i++;
}
return i;
}

/**
* Checks whether the {@code needle} matches {@code src} at offset
* {@code from}.
*
* @param src
* to match against {@code needle}
* @param from
* position in {@code src} to start matching
* @param needle
* to match against
* @return {@code true} if {@code src} contains {@code needle} at position
* {@code from}, {@code false} otherwise
*/
private static boolean matches(byte[] src, int from, byte[] needle) {
if (from < 0 || from + needle.length > src.length) {
return false;
}
return org.bouncycastle.util.Arrays.constantTimeAreEqual(needle.length,
src, from, needle, 0);
}

/**
* Converts a human-readable serialized s-expression into a binary
* serialized s-expression.
*
* @param humanForm
* to convert
* @return the converted s-expression
* @throws IOException
* if the conversion fails
*/
private static byte[] convertSexpression(byte[] humanForm)
throws IOException {
boolean[] isOCB = { false };
return convertSexpression(humanForm, isOCB);
}

/**
* Converts a human-readable serialized s-expression into a binary
* serialized s-expression.
*
* @param humanForm
* to convert
* @param isOCB
* returns whether the s-expression specified AES/OCB encryption
* @return the converted s-expression
* @throws IOException
* if the conversion fails
*/
private static byte[] convertSexpression(byte[] humanForm, boolean[] isOCB)
throws IOException {
int pos = 0;
try (ByteArrayOutputStream out = new ByteArrayOutputStream(
humanForm.length)) {
while (pos < humanForm.length) {
byte b = humanForm[pos];
if (b == '(' || b == ')') {
out.write(b);
pos++;
} else if (isGpgSpace(b)) {
pos++;
} else if (b == '#') {
// Hex value follows up to the next #
int i = ++pos;
while (i < humanForm.length && isHex(humanForm[i])) {
i++;
}
if (i == pos || humanForm[i] != '#') {
throw new StreamCorruptedException(
BCText.get().sexprHexNotClosed);
}
if ((i - pos) % 2 != 0) {
throw new StreamCorruptedException(
BCText.get().sexprHexOdd);
}
int l = (i - pos) / 2;
out.write(Integer.toString(l)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
while (pos < i) {
int x = (nibble(humanForm[pos]) << 4)
| nibble(humanForm[pos + 1]);
pos += 2;
out.write(x);
}
pos = i + 1;
} else if (isTokenChar(b)) {
// Scan the token
int start = pos++;
while (pos < humanForm.length
&& isTokenChar(humanForm[pos])) {
pos++;
}
int l = pos - start;
if (pos - start == OCB_PROTECTED.length
&& matches(humanForm, start, OCB_PROTECTED)) {
isOCB[0] = true;
}
out.write(Integer.toString(l)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
out.write(humanForm, start, pos - start);
} else if (b == '"') {
// Potentially quoted string.
int start = ++pos;
boolean escaped = false;
while (pos < humanForm.length
&& (escaped || humanForm[pos] != '"')) {
int ch = humanForm[pos++];
escaped = !escaped && ch == '\\';
}
if (pos >= humanForm.length) {
throw new StreamCorruptedException(
BCText.get().sexprStringNotClosed);
}
// start is on the first character of the string, pos on the
// closing quote.
byte[] dq = dequote(humanForm, start, pos);
out.write(Integer.toString(dq.length)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
out.write(dq);
pos++;
} else {
throw new StreamCorruptedException(
MessageFormat.format(BCText.get().sexprUnhandled,
Integer.toHexString(b & 0xFF)));
}
}
return out.toByteArray();
}
}

/**
* GPG-style string de-quoting, which is basically C-style, with some
* literal CR/LF escaping.
*
* @param in
* buffer containing the quoted string
* @param from
* index after the opening quote in {@code in}
* @param to
* index of the closing quote in {@code in}
* @return the dequoted raw string value
* @throws StreamCorruptedException
*/
private static byte[] dequote(byte[] in, int from, int to)
throws StreamCorruptedException {
// Result must be shorter or have the same length
byte[] out = new byte[to - from];
int j = 0;
int i = from;
while (i < to) {
byte b = in[i++];
if (b != '\\') {
out[j++] = b;
continue;
}
if (i == to) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidEscapeAtEnd);
}
b = in[i++];
switch (b) {
case 'b':
out[j++] = '\b';
break;
case 'f':
out[j++] = '\f';
break;
case 'n':
out[j++] = '\n';
break;
case 'r':
out[j++] = '\r';
break;
case 't':
out[j++] = '\t';
break;
case 'v':
out[j++] = 0x0B;
break;
case '"':
case '\'':
case '\\':
out[j++] = b;
break;
case '\r':
// Escaped literal line end. If an LF is following, skip that,
// too.
if (i < to && in[i] == '\n') {
i++;
}
break;
case '\n':
// Same for LF possibly followed by CR.
if (i < to && in[i] == '\r') {
i++;
}
break;
case 'x':
if (i + 1 >= to || !isHex(in[i]) || !isHex(in[i + 1])) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidHexEscape);
}
out[j++] = (byte) ((nibble(in[i]) << 4) | nibble(in[i + 1]));
i += 2;
break;
case '0':
case '1':
case '2':
case '3':
if (i + 2 >= to || !isOctal(in[i]) || !isOctal(in[i + 1])
|| !isOctal(in[i + 2])) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidOctalEscape);
}
out[j++] = (byte) (((((in[i] - '0') << 3)
| (in[i + 1] - '0')) << 3) | (in[i + 2] - '0'));
i += 3;
break;
default:
throw new StreamCorruptedException(MessageFormat.format(
BCText.get().sexprStringInvalidEscape,
Integer.toHexString(b & 0xFF)));
}
}
return Arrays.copyOf(out, j);
}

/**
* Extracts the key from a GPG name-value-pair key file.
* <p>
* Package-visible for tests only.
* </p>
*
* @param in
* {@link InputStream} to read from; should be buffered
* @return the raw key data as extracted from the file
* @throws IOException
* if the {@code in} stream cannot be read or does not contain a
* key
*/
static byte[] keyFromNameValueFormat(InputStream in) throws IOException {
// It would be nice if we could use RawParseUtils here, but GPG compares
// names case-insensitively. We're only interested in the "Key:"
// name-value pair.
int[] nameLow = { 'k', 'e', 'y', ':' };
int[] nameCap = { 'K', 'E', 'Y', ':' };
int nameIdx = 0;
for (;;) {
int next = in.read();
if (next < 0) {
throw new EOFException();
}
if (next == '\n') {
nameIdx = 0;
} else if (nameIdx >= 0) {
if (nameLow[nameIdx] == next || nameCap[nameIdx] == next) {
nameIdx++;
if (nameIdx == nameLow.length) {
break;
}
} else {
nameIdx = -1;
}
}
}
// We're after "Key:". Read the value as continuation lines.
int last = ':';
byte[] rawData;
try (ByteArrayOutputStream out = new ByteArrayOutputStream(8192)) {
for (;;) {
int next = in.read();
if (next < 0) {
break;
}
if (last == '\n') {
if (next == ' ' || next == '\t') {
// Continuation line; skip this whitespace
last = next;
continue;
}
break; // Not a continuation line
}
out.write(next);
last = next;
}
rawData = out.toByteArray();
}
// GPG trims off trailing whitespace, and a line having only whitespace
// is a single LF.
try (ByteArrayOutputStream out = new ByteArrayOutputStream(
rawData.length)) {
int lineStart = 0;
boolean trimLeading = true;
while (lineStart < rawData.length) {
int nextLineStart = RawParseUtils.nextLF(rawData, lineStart);
if (trimLeading) {
while (lineStart < nextLineStart
&& isGpgSpace(rawData[lineStart])) {
lineStart++;
}
}
// Trim trailing
int i = nextLineStart - 1;
while (lineStart < i && isGpgSpace(rawData[i])) {
i--;
}
if (i <= lineStart) {
// Empty line signifies LF
out.write('\n');
trimLeading = true;
} else {
out.write(rawData, lineStart, i - lineStart + 1);
trimLeading = false;
}
lineStart = nextLineStart;
}
return out.toByteArray();
}
}

private static boolean isGpgSpace(int ch) {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
}

private static boolean isTokenChar(int ch) {
switch (ch) {
case '-':
case '.':
case '/':
case '_':
case ':':
case '*':
case '+':
case '=':
return true;
default:
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9')) {
return true;
}
return false;
}
}

private static boolean isHex(int ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
|| (ch >= 'a' && ch <= 'f');
}

private static boolean isOctal(int ch) {
return (ch >= '0' && ch <= '7');
}

private static int nibble(int ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
} else if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
}
return -1;
}
}

Loading…
Cancel
Save