aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF4
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc41
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key42
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc42
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key45
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc30
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key32
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key23
-rw-r--r--org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java155
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF8
-rw-r--r--org.eclipse.jgit.gpg.bc/about.html83
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties12
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java12
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java51
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java7
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java10
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java121
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java826
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java110
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java597
20 files changed, 2157 insertions, 94 deletions
diff --git a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
index 39ece1fcf0..57c3747954 100644
--- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
@@ -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)"
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc
new file mode 100644
index 0000000000..355462c098
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc
@@ -0,0 +1,41 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+ZNx6wjRReIvT
+=8E/K
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key
new file mode 100644
index 0000000000..afa459c838
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key
@@ -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
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc
new file mode 100644
index 0000000000..362e2108ed
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc
@@ -0,0 +1,42 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+qnA=
+=03Wd
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key
new file mode 100644
index 0000000000..cef72f6234
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key
@@ -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
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc
new file mode 100644
index 0000000000..c6e0408b3e
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc
@@ -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-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key
new file mode 100644
index 0000000000..63617c09bf
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key
@@ -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
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key
new file mode 100644
index 0000000000..405afad427
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key
@@ -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)
+ ))) \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
new file mode 100644
index 0000000000..4eecaf3ab5
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
@@ -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());
+ }
+ }
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index 040ed0818c..afb0ee151f 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -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",
diff --git a/org.eclipse.jgit.gpg.bc/about.html b/org.eclipse.jgit.gpg.bc/about.html
index f971af18d0..fc527d5a3a 100644
--- a/org.eclipse.jgit.gpg.bc/about.html
+++ b/org.eclipse.jgit.gpg.bc/about.html
@@ -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>
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index f2aa014d6b..e4b1baba1f 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -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
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 4753ac138d..aedf8a5be5 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -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;
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
index 7f0f32a2a7..cf4d3d2340 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
@@ -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()) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
index e47f64f1a6..6144195983 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
@@ -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) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index 9f48e5431e..211bd7bd20 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -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(
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
new file mode 100644
index 0000000000..68f8a45555
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
@@ -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);
+ }
+ }
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
new file mode 100644
index 0000000000..a9bb22c780
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
@@ -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(")"));
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
new file mode 100644
index 0000000000..220aa285ff
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
@@ -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$
+ }
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
new file mode 100644
index 0000000000..1542b8cbcc
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
@@ -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;
+ }
+}