@@ -1196,31 +1144,33 @@ public static PgpSecretKey ParseSecretKeyFromSExprUtf8(Stream inputStream, char[
///
public static PgpSecretKey ParseSecretKeyFromSExprRaw(Stream inputStream, byte[] rawPassPhrase)
{
- return DoParseSecretKeyFromSExpr(inputStream, rawPassPhrase, false);
+ return DoParseSecretKeyFromSExpr(inputStream, rawPassPhrase, false, null);
}
///
/// Parse a secret key from one of the GPG S expression keys.
///
- internal static PgpSecretKey DoParseSecretKeyFromSExpr(Stream inputStream, byte[] rawPassPhrase, bool clearPassPhrase)
+ internal static PgpSecretKey DoParseSecretKeyFromSExpr(Stream inputStream, byte[] rawPassPhrase, bool clearPassPhrase, PgpPublicKey pubKey)
{
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ SXprReader reader = new SXprReader(inputStream);
- string type = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ reader.SkipOpenParenthesis();
+
+ string type = reader.ReadString();
if (type.Equals("protected-private-key"))
{
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ reader.SkipOpenParenthesis();
string curveName;
Oid curveOid;
- string keyType = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ string keyType = reader.ReadString();
if (keyType.Equals("ecc"))
{
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ reader.SkipOpenParenthesis();
- string curveID = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
- curveName = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ string curveID = reader.ReadString();
+ curveName = reader.ReadString();
switch (curveName)
{
@@ -1230,12 +1180,13 @@ internal static PgpSecretKey DoParseSecretKeyFromSExpr(Stream inputStream, byte[
case "brainpoolP256r1": curveOid = new Oid("1.3.36.3.3.2.8.1.1.7"); break;
case "brainpoolP384r1": curveOid = new Oid("1.3.36.3.3.2.8.1.1.11"); break;
case "brainpoolP512r1": curveOid = new Oid("1.3.36.3.3.2.8.1.1.13"); break;
- // FIXME: curve25519
+ case "Curve25519": curveOid = new Oid("1.3.6.1.4.1.3029.1.5.1"); break;
+ case "Ed25519": curveOid = new Oid("1.3.6.1.4.1.11591.15.1"); break;
default:
throw new PgpException("unknown curve algorithm");
}
- SXprUtilities.SkipCloseParenthesis(inputStream);
+ reader.SkipCloseParenthesis();
}
else
{
@@ -1243,79 +1194,183 @@ internal static PgpSecretKey DoParseSecretKeyFromSExpr(Stream inputStream, byte[
}
byte[] qVal;
+ string flags = null;
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ reader.SkipOpenParenthesis();
- type = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ type = reader.ReadString();
+ if (type == "flags")
+ {
+ // Skip over flags
+ flags = reader.ReadString();
+ reader.SkipCloseParenthesis();
+ reader.SkipOpenParenthesis();
+ type = reader.ReadString();
+ }
if (type.Equals("q"))
{
- qVal = SXprUtilities.ReadBytes(inputStream, inputStream.ReadByte());
+ qVal = reader.ReadBytes();
}
else
{
throw new PgpException("no q value found");
}
- PublicKeyPacket pubPacket = new PublicKeyPacket(PublicKeyAlgorithmTag.ECDsa, DateTime.UtcNow,
- new ECDsaPublicBcpgKey(curveOid, new MPInteger(qVal)));
+ if (pubKey == null)
+ {
+ PublicKeyPacket pubPacket = new PublicKeyPacket(
+ flags == "eddsa" ? PublicKeyAlgorithmTag.EdDsa : PublicKeyAlgorithmTag.ECDsa, DateTime.UtcNow,
+ new ECDsaPublicBcpgKey(curveOid, new MPInteger(qVal)));
+ pubKey = new PgpPublicKey(pubPacket);
+ }
- SXprUtilities.SkipCloseParenthesis(inputStream);
+ reader.SkipCloseParenthesis();
- byte[] dValue = GetDValue(inputStream, rawPassPhrase, clearPassPhrase, curveName);
- // TODO: check SHA-1 hash.
+ byte[] dValue = GetDValue(reader, pubKey.PublicKeyPacket, rawPassPhrase, clearPassPhrase, curveName);
- return new PgpSecretKey(new SecretKeyPacket(pubPacket, SymmetricKeyAlgorithmTag.Null, null, null,
- new ECSecretBcpgKey(new MPInteger(dValue)).GetEncoded()), new PgpPublicKey(pubPacket));
+ return new PgpSecretKey(new SecretKeyPacket(pubKey.PublicKeyPacket, SymmetricKeyAlgorithmTag.Null, null, null,
+ new ECSecretBcpgKey(new MPInteger(dValue)).GetEncoded()), pubKey);
}
throw new PgpException("unknown key type found");
}
- private static byte[] GetDValue(Stream inputStream, byte[] rawPassPhrase, bool clearPassPhrase, string curveName)
+ private static void WriteSExprPublicKey(SXprWriter writer, PublicKeyPacket pubPacket, string curveName, string protectedAt)
+ {
+ writer.StartList();
+ switch (pubPacket.Algorithm)
+ {
+ case PublicKeyAlgorithmTag.ECDsa:
+ case PublicKeyAlgorithmTag.EdDsa:
+ writer.WriteString("ecc");
+ writer.StartList();
+ writer.WriteString("curve");
+ writer.WriteString(curveName);
+ writer.EndList();
+ if (pubPacket.Algorithm == PublicKeyAlgorithmTag.EdDsa)
+ {
+ writer.StartList();
+ writer.WriteString("flags");
+ writer.WriteString("eddsa");
+ writer.EndList();
+ }
+ writer.StartList();
+ writer.WriteString("q");
+ writer.WriteBytes(((ECDsaPublicBcpgKey)pubPacket.Key).EncodedPoint.Value);
+ writer.EndList();
+ break;
+
+ case PublicKeyAlgorithmTag.RsaEncrypt:
+ case PublicKeyAlgorithmTag.RsaSign:
+ case PublicKeyAlgorithmTag.RsaGeneral:
+ RsaPublicBcpgKey rsaK = (RsaPublicBcpgKey)pubPacket.Key;
+ writer.WriteString("rsa");
+ writer.StartList();
+ writer.WriteString("n");
+ writer.WriteBytes(rsaK.Modulus.Value);
+ writer.EndList();
+ writer.StartList();
+ writer.WriteString("e");
+ writer.WriteBytes(rsaK.PublicExponent.Value);
+ writer.EndList();
+ break;
+
+ // TODO: DSA, etc.
+ default:
+ throw new PgpException("unsupported algorithm in S expression");
+ }
+
+ if (protectedAt != null)
+ {
+ writer.StartList();
+ writer.WriteString("protected-at");
+ writer.WriteString(protectedAt);
+ writer.EndList();
+ }
+ writer.EndList();
+ }
+
+ private static byte[] GetDValue(SXprReader reader, PublicKeyPacket publicKey, byte[] rawPassPhrase, bool clearPassPhrase, string curveName)
{
string type;
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ reader.SkipOpenParenthesis();
string protection;
+ string protectedAt = null;
S2k s2k;
byte[] iv;
byte[] secKeyData;
- type = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ type = reader.ReadString();
if (type.Equals("protected"))
{
- protection = SXprUtilities.ReadString(inputStream, inputStream.ReadByte());
+ protection = reader.ReadString();
+
+ reader.SkipOpenParenthesis();
+
+ s2k = reader.ParseS2k();
+
+ iv = reader.ReadBytes();
- SXprUtilities.SkipOpenParenthesis(inputStream);
+ reader.SkipCloseParenthesis();
- s2k = SXprUtilities.ParseS2k(inputStream);
+ secKeyData = reader.ReadBytes();
- iv = SXprUtilities.ReadBytes(inputStream, inputStream.ReadByte());
+ reader.SkipCloseParenthesis();
- SXprUtilities.SkipCloseParenthesis(inputStream);
+ reader.SkipOpenParenthesis();
- secKeyData = SXprUtilities.ReadBytes(inputStream, inputStream.ReadByte());
+ if (reader.ReadString().Equals("protected-at"))
+ {
+ protectedAt = reader.ReadString();
+ }
}
else
{
throw new PgpException("protected block not found");
}
- // TODO: recognise other algorithms
- byte[] key = PgpUtilities.DoMakeKeyFromPassPhrase(SymmetricKeyAlgorithmTag.Aes128, s2k, rawPassPhrase, clearPassPhrase);
+ byte[] data;
+ byte[] key;
+
+ switch (protection)
+ {
+ case "openpgp-s2k3-sha1-aes256-cbc":
+ case "openpgp-s2k3-sha1-aes-cbc":
+ SymmetricKeyAlgorithmTag symmAlg =
+ protection.Equals("openpgp-s2k3-sha1-aes256-cbc") ? SymmetricKeyAlgorithmTag.Aes256 : SymmetricKeyAlgorithmTag.Aes128;
+ key = PgpUtilities.DoMakeKeyFromPassPhrase(symmAlg, s2k, rawPassPhrase, clearPassPhrase);
+ data = RecoverKeyData(symmAlg, CipherMode.CBC, key, iv, secKeyData, 0, secKeyData.Length);
+ // TODO: check SHA-1 hash.
+ break;
- byte[] data = RecoverKeyData(SymmetricKeyAlgorithmTag.Aes128, CipherMode.CBC, key, iv, secKeyData, 0, secKeyData.Length);
+ case "openpgp-s2k3-ocb-aes":
+ MemoryStream aad = new MemoryStream();
+ WriteSExprPublicKey(new SXprWriter(aad), publicKey, curveName, protectedAt);
+ key = PgpUtilities.DoMakeKeyFromPassPhrase(SymmetricKeyAlgorithmTag.Aes128, s2k, rawPassPhrase, clearPassPhrase);
+ /*IBufferedCipher c = CipherUtilities.GetCipher("AES/OCB");
+ c.Init(false, new AeadParameters(key, 128, iv, aad.ToArray()));
+ data = c.DoFinal(secKeyData, 0, secKeyData.Length);*/
+ // TODO: AES/OCB support
+ throw new NotImplementedException();
+ break;
+
+ case "openpgp-native":
+ default:
+ throw new PgpException(protection + " key format is not supported yet");
+ }
//
// parse the secret key S-expr
//
Stream keyIn = new MemoryStream(data, false);
- SXprUtilities.SkipOpenParenthesis(keyIn);
- SXprUtilities.SkipOpenParenthesis(keyIn);
- SXprUtilities.SkipOpenParenthesis(keyIn);
- String name = SXprUtilities.ReadString(keyIn, keyIn.ReadByte());
- return SXprUtilities.ReadBytes(keyIn, keyIn.ReadByte());
+ reader = new SXprReader(keyIn);
+ reader.SkipOpenParenthesis();
+ reader.SkipOpenParenthesis();
+ reader.SkipOpenParenthesis();
+ String name = reader.ReadString();
+ return reader.ReadBytes();
}
}
}
diff --git a/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprReader.cs b/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprReader.cs
new file mode 100644
index 0000000..6faf14c
--- /dev/null
+++ b/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprReader.cs
@@ -0,0 +1,276 @@
+using System;
+using System.IO;
+using System.Text;
+using Org.BouncyCastle.Utilities.IO;
+
+namespace Org.BouncyCastle.Bcpg.OpenPgp
+{
+ ///
+ /// Reader for S-expression keys. This class will move when it finds a better home!
+ ///
+ ///
+ /// 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
+ /// http://people.csail.mit.edu/rivest/Sexp.txt
+ ///
+ class SXprReader
+ {
+ Stream stream;
+ int peekedByte;
+
+ public SXprReader(Stream stream)
+ {
+ this.stream = stream;
+ this.peekedByte = -1;
+ }
+
+ private int ReadByte()
+ {
+ if (this.peekedByte > 0)
+ {
+ int pb = this.peekedByte;
+ this.peekedByte = 0;
+ return pb;
+ }
+ return stream.ReadByte();
+ }
+
+ private void UnreadByte(int pb)
+ {
+ this.peekedByte = pb;
+ }
+
+ private int ReadLength()
+ {
+ int ch;
+ int len = 0;
+
+ while ((ch = ReadByte()) >= 0 && ch >= '0' && ch <= '9')
+ {
+ len = len * 10 + (ch - '0');
+ }
+ UnreadByte(ch);
+
+ return len;
+ }
+
+ public string ReadString()
+ {
+ SkipWhitespace();
+
+ int ch = ReadByte();
+ if (ch >= '0' && ch <= '9')
+ {
+ UnreadByte(ch);
+
+ int len = ReadLength();
+ ch = ReadByte();
+ if (ch == ':')
+ {
+ char[] chars = new char[len];
+
+ for (int i = 0; i != chars.Length; i++)
+ {
+ chars[i] = (char)ReadByte();
+ }
+
+ return new string(chars);
+ }
+ else if (ch == '"')
+ {
+ return ReadQuotedString(len);
+ }
+ throw new IOException("unsupported encoding");
+ }
+ else if (ch == '"')
+ {
+ return ReadQuotedString(0);
+ }
+ else if (ch == '{' || ch == '|' || ch == '#')
+ {
+ // TODO: Unsupported encoding
+ throw new IOException("unsupported encoding");
+ }
+ else
+ {
+ StringBuilder sb = new StringBuilder();
+ while (IsTokenChar(ch))
+ {
+ sb.Append((char)ch);
+ ch = (char)ReadByte();
+ }
+ UnreadByte(ch);
+ return sb.ToString();
+ }
+ }
+
+ private string ReadQuotedString(int length)
+ {
+ StringBuilder sb = new StringBuilder(length);
+ int ch;
+ bool skipNewLine = false;
+ do
+ {
+ ch = ReadByte();
+ if ((ch == '\n' || ch == '\r') && skipNewLine)
+ {
+ skipNewLine = false;
+ }
+ else if (ch == '\\')
+ {
+ ch = (char)ReadByte();
+ switch (ch)
+ {
+ case 'b': sb.Append('\b'); break;
+ case 't': sb.Append('\t'); break;
+ case 'v': sb.Append('\v'); break;
+ case 'n': sb.Append('\n'); break;
+ case 'r': sb.Append('\r'); break;
+ case 'f': sb.Append('\f'); break;
+ case '"': sb.Append('"'); break;
+ case '\'': sb.Append('\''); break;
+ case '\r':
+ case '\n':
+ skipNewLine = true;
+ break;
+ default:
+ // TODO: Octal value, hexadecimal value
+ throw new IOException("unsupported encoding");
+ }
+ }
+ else if (ch != '"' && ch >= 0)
+ {
+ skipNewLine = false;
+ sb.Append((char)ch);
+ }
+ }
+ while (ch != '"' && ch > 0);
+ return sb.ToString();
+ }
+
+ private static bool IsTokenChar(int ch)
+ {
+ return
+ (ch >= 'a' && ch <= 'z') ||
+ (ch >= 'A' && ch <= 'Z') ||
+ (ch >= '0' && ch <= '9') ||
+ ch == '-' || ch == '.' ||
+ ch == '/' || ch == '_' ||
+ ch == ':' || ch == '*' ||
+ ch == '+' || ch == '=';
+ }
+
+ public byte[] ReadBytes()
+ {
+ SkipWhitespace();
+
+ int ch = ReadByte();
+ if (ch >= '0' && ch <= '9')
+ {
+ UnreadByte(ch);
+
+ int len = ReadLength();
+
+ if (ReadByte() != ':')
+ throw new IOException("unsupported encoding");
+
+ byte[] data = new byte[len];
+
+ Streams.ReadFully(stream, data);
+
+ return data;
+ }
+ else if (ch == '#')
+ {
+ MemoryStream bytes = new MemoryStream();
+ do
+ {
+ ch = ReadByte();
+ if (ch == '#')
+ break;
+ int digit0 = HexToNumber(ch);
+ if (digit0 < 0)
+ throw new IOException("invalid hex encoding");
+ ch = ReadByte();
+ int digit1 = HexToNumber(ch);
+ if (digit1 < 0)
+ throw new IOException("invalid hex encoding");
+ bytes.WriteByte((byte)((digit0 << 8) + digit1));
+ }
+ while (ch != '#' && ch >= 0);
+ return bytes.ToArray();
+ }
+
+ throw new IOException("unsupported encoding");
+ }
+
+ private static int HexToNumber(int c)
+ {
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 0xa;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 0xa;
+ return -1;
+ }
+
+ public S2k ParseS2k()
+ {
+ SkipOpenParenthesis();
+
+ string alg = ReadString();
+ byte[] iv = ReadBytes();
+ long iterationCount = Int64.Parse(ReadString());
+
+ SkipCloseParenthesis();
+
+ // we have to return the actual iteration count provided.
+ return new MyS2k(HashAlgorithmTag.Sha1, iv, iterationCount);
+ }
+
+ public void SkipWhitespace()
+ {
+ int ch = ReadByte();
+ while (ch == ' ' || ch == '\r' || ch == '\n')
+ {
+ ch = ReadByte();
+ }
+ UnreadByte(ch);
+ }
+
+ public void SkipOpenParenthesis()
+ {
+ SkipWhitespace();
+
+ int ch = ReadByte();
+ if (ch != '(')
+ throw new IOException("unknown character encountered");
+ }
+
+ public void SkipCloseParenthesis()
+ {
+ SkipWhitespace();
+
+ int ch = ReadByte();
+ if (ch != ')')
+ throw new IOException("unknown character encountered");
+ }
+
+ private class MyS2k : S2k
+ {
+ private readonly long mIterationCount64;
+
+ internal MyS2k(HashAlgorithmTag algorithm, byte[] iv, long iterationCount64)
+ : base(algorithm, iv, (int)iterationCount64)
+ {
+ this.mIterationCount64 = iterationCount64;
+ }
+
+ public override long IterationCount
+ {
+ get { return mIterationCount64; }
+ }
+ }
+ }
+}
diff --git a/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprUtilities.cs b/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprUtilities.cs
deleted file mode 100644
index 68ff373..0000000
--- a/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprUtilities.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.IO;
-
-using Org.BouncyCastle.Utilities.IO;
-
-namespace Org.BouncyCastle.Bcpg.OpenPgp
-{
- /**
- * Utility functions for looking a S-expression keys. This class will move when it finds a better home!
- *
- * 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
- *
- */
- public sealed class SXprUtilities
- {
- private SXprUtilities()
- {
- }
-
- private static int ReadLength(Stream input, int ch)
- {
- int len = ch - '0';
-
- while ((ch = input.ReadByte()) >= 0 && ch != ':')
- {
- len = len * 10 + ch - '0';
- }
-
- return len;
- }
-
- internal static string ReadString(Stream input, int ch)
- {
- int len = ReadLength(input, ch);
-
- char[] chars = new char[len];
-
- for (int i = 0; i != chars.Length; i++)
- {
- chars[i] = (char)input.ReadByte();
- }
-
- return new string(chars);
- }
-
- internal static byte[] ReadBytes(Stream input, int ch)
- {
- int len = ReadLength(input, ch);
-
- byte[] data = new byte[len];
-
- Streams.ReadFully(input, data);
-
- return data;
- }
-
- internal static S2k ParseS2k(Stream input)
- {
- SkipOpenParenthesis(input);
-
- string alg = ReadString(input, input.ReadByte());
- byte[] iv = ReadBytes(input, input.ReadByte());
- long iterationCount = Int64.Parse(ReadString(input, input.ReadByte()));
-
- SkipCloseParenthesis(input);
-
- // we have to return the actual iteration count provided.
- return new MyS2k(HashAlgorithmTag.Sha1, iv, iterationCount);
- }
-
- internal static void SkipOpenParenthesis(Stream input)
- {
- int ch = input.ReadByte();
- if (ch != '(')
- throw new IOException("unknown character encountered");
- }
-
- internal static void SkipCloseParenthesis(Stream input)
- {
- int ch = input.ReadByte();
- if (ch != ')')
- throw new IOException("unknown character encountered");
- }
-
- private class MyS2k : S2k
- {
- private readonly long mIterationCount64;
-
- internal MyS2k(HashAlgorithmTag algorithm, byte[] iv, long iterationCount64)
- : base(algorithm, iv, (int)iterationCount64)
- {
- this.mIterationCount64 = iterationCount64;
- }
-
- public override long IterationCount
- {
- get { return mIterationCount64; }
- }
- }
- }
-}
diff --git a/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprWriter.cs b/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprWriter.cs
new file mode 100644
index 0000000..640c4b6
--- /dev/null
+++ b/src/Org/BouncyCastle/Bcpg/OpenPgp/SXprWriter.cs
@@ -0,0 +1,50 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Org.BouncyCastle.Bcpg.OpenPgp
+{
+ /**
+ * Writer for S-expression keys
+ *
+ * Format documented here:
+ * http://people.csail.mit.edu/rivest/Sexp.txt
+ *
+ * Only canonical S expression format is used.
+ *
+ */
+ class SXprWriter
+ {
+ Stream output;
+
+ public SXprWriter(Stream output)
+ {
+ this.output = output;
+ }
+
+ public void StartList()
+ {
+ output.WriteByte((byte)'(');
+ }
+
+ public void EndList()
+ {
+ output.WriteByte((byte)')');
+ }
+
+ public void WriteString(string s)
+ {
+ byte[] stringBytes = Encoding.UTF8.GetBytes(s);
+ byte[] lengthBytes = Encoding.UTF8.GetBytes(stringBytes.Length + ":");
+ output.Write(lengthBytes, 0, lengthBytes.Length);
+ output.Write(stringBytes, 0, stringBytes.Length);
+ }
+
+ public void WriteBytes(byte[] b)
+ {
+ byte[] lengthBytes = Encoding.UTF8.GetBytes(b.Length + ":");
+ output.Write(lengthBytes, 0, lengthBytes.Length);
+ output.Write(b, 0, b.Length);
+ }
+ }
+}