diff --git a/IpfsCli/Commands/AddCommand.cs b/IpfsCli/Commands/AddCommand.cs index 90e46377..d29acea0 100644 --- a/IpfsCli/Commands/AddCommand.cs +++ b/IpfsCli/Commands/AddCommand.cs @@ -44,6 +44,9 @@ class AddCommand : CommandBase [Option("-r|--recursive", Description = "Add directory paths recursively")] public bool Recursive { get; set; } + [Option("--protect", Description = "protect the data with the key")] + public string ProtectionKey { get; set; } + Program Parent { get; set; } protected override async Task OnExecute(CommandLineApplication app) @@ -57,7 +60,8 @@ protected override async Task OnExecute(CommandLineApplication app) Pin = Pin, RawLeaves = RawLeaves, Trickle = Trickle, - Wrap = Wrap + Wrap = Wrap, + ProtectionKey = ProtectionKey }; IFileSystemNode node; if (Directory.Exists(FilePath)) diff --git a/IpfsCli/IpfsCli.csproj b/IpfsCli/IpfsCli.csproj index 20ef5ccd..3e34515a 100644 --- a/IpfsCli/IpfsCli.csproj +++ b/IpfsCli/IpfsCli.csproj @@ -15,7 +15,7 @@ - + diff --git a/IpfsServer/HttpApi/V0/FileSystemController.cs b/IpfsServer/HttpApi/V0/FileSystemController.cs index 3525394f..75d3fdaf 100644 --- a/IpfsServer/HttpApi/V0/FileSystemController.cs +++ b/IpfsServer/HttpApi/V0/FileSystemController.cs @@ -209,7 +209,8 @@ public async Task Add( bool pin = false, [ModelBinder(Name = "raw-leaves")] bool rawLeaves = false, bool trickle = false, - [ModelBinder(Name = "wrap-with-directory")] bool wrap = false + [ModelBinder(Name = "wrap-with-directory")] bool wrap = false, + string protect = null ) { if (file == null) @@ -223,7 +224,8 @@ public async Task Add( Pin = pin, RawLeaves = rawLeaves, Trickle = trickle, - Wrap = wrap + Wrap = wrap, + ProtectionKey = protect }; if (chunker != null) { diff --git a/IpfsServer/IpfsServer.csproj b/IpfsServer/IpfsServer.csproj index f5b8c941..574cccf8 100644 --- a/IpfsServer/IpfsServer.csproj +++ b/IpfsServer/IpfsServer.csproj @@ -13,7 +13,7 @@ - + diff --git a/PeerTalk/src/PeerTalk.csproj b/PeerTalk/src/PeerTalk.csproj index d6933a54..d0cca579 100644 --- a/PeerTalk/src/PeerTalk.csproj +++ b/PeerTalk/src/PeerTalk.csproj @@ -37,14 +37,14 @@ - + - + diff --git a/doc/articles/filesystem.md b/doc/articles/filesystem.md index cc97db22..a9e96230 100644 --- a/doc/articles/filesystem.md +++ b/doc/articles/filesystem.md @@ -9,15 +9,9 @@ and [Object API](xref:Ipfs.CoreApi.IObjectApi). A file has a unique [content id (CID)](xref:Ipfs.Cid) which is the cryptographic hash of the content; see [CID concept](https://docs.ipfs.io/guides/concepts/cid/) for background information. The file's content is not just the file's data but is encapsulated with a [protocol buffer](https://en.wikipedia.org/wiki/Protocol_Buffers) encoding of the -[PBNode](https://github.com/ipfs/go-ipfs/blob/0cb22ccf359e05fb5b55a9bf2f9c515bf7d4dba7/merkledag/pb/merkledag.proto#L31-L39) +[Merkle DAG](https://github.com/ipfs/go-ipfs/blob/0cb22ccf359e05fb5b55a9bf2f9c515bf7d4dba7/merkledag/pb/merkledag.proto#L31-L39) and [UnixFS Data](https://github.com/ipfs/go-ipfs/blob/0cb22ccf359e05fb5b55a9bf2f9c515bf7d4dba7/unixfs/pb/unixfs.proto#L3-L20). -Where -- `PBNode.Data` contains unixfs message Data -- unixfs `Data.Data` contans file's data - -When the file's data exceeds the [chunking size](xref:Ipfs.CoreApi.AddFileOptions.ChunkSize), multiple [blocks](xref:Ipfs.CoreApi.IBlockApi) -are generated. The returned CID points to a block that has `PBNode.Links` and no `PBNode.Data`. ### Adding a file @@ -47,17 +41,3 @@ using (var stream = await ipfs.FileSystem.ReadFileAsyc(path)) // Do something with the data } ``` - -### Getting a CID - -Normally, you get the CID by [adding](xref:Ipfs.CoreApi.IFileSystemApi.AddAsync*) the file to IPFS. You can avoid adding it -to IPFS by using the [OnlyHash option](xref:Ipfs.CoreApi.AddFileOptions.OnlyHash). - -```csharp -var options = new AddFileOptions { OnlyHash = true }; -var fsn = await ipfs.FileSystem.AddTextAsync("hello world", options); -Console.WriteLine((string)fsn.Id) - -// Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD -``` - diff --git a/doc/articles/fs/cid-only.md b/doc/articles/fs/cid-only.md new file mode 100644 index 00000000..2fceacb0 --- /dev/null +++ b/doc/articles/fs/cid-only.md @@ -0,0 +1,13 @@ +### Getting a CID + +Normally, you get the CID by [adding](xref:Ipfs.CoreApi.IFileSystemApi.AddAsync*) the file to IPFS. You can avoid adding it +to IPFS by using the [OnlyHash option](xref:Ipfs.CoreApi.AddFileOptions.OnlyHash). + +```csharp +var options = new AddFileOptions { OnlyHash = true }; +var fsn = await ipfs.FileSystem.AddTextAsync("hello world", options); +Console.WriteLine((string)fsn.Id) + +// Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD +``` + diff --git a/doc/articles/fs/encryption.md b/doc/articles/fs/encryption.md new file mode 100644 index 00000000..3987b89f --- /dev/null +++ b/doc/articles/fs/encryption.md @@ -0,0 +1,29 @@ +# Encryption + +The [protection key option](xref:Ipfs.CoreApi.AddFileOptions.ProtectionKey) specifies that +the file's data blocks are encrypted using the specified [key name](../key.md). + +Each data block maps to a [RFC652 - Cryptographic Message Syntax (CMS)](https://tools.ietf.org/html/rfc5652) +of type [Enveloped-data](https://tools.ietf.org/html/rfc5652#section-6) which is DER encoded +and has the following features: + +- The data block is encrypted with a random IV and key using [aes-256-cbc](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) +- The recipient is a key transport (ktri) with the Subject Key ID equal to the protection key's public ID +- The protection key is used to obtain the `aes key` to decrypt the data block. + +The [Cid.ContentType](xref:Ipfs.Cid.ContentType) is set to `cms`. + +```csharp +var options = new AddFileOptions +{ + ProtectionKey = "me" +}; +var node = await ipfs.FileSystem.AddTextAsync("hello world", options); +``` + +## Reading + +The standard [read file methods](../filesystem.md#reading-a-file) are used to decrypt to file. +If the private key is not held by the local peer, then a `KeyNotFoundException` is thrown. + + diff --git a/doc/articles/fs/format.md b/doc/articles/fs/format.md new file mode 100644 index 00000000..6bde7687 --- /dev/null +++ b/doc/articles/fs/format.md @@ -0,0 +1,71 @@ +### Standard Format + +Here is the [Merkle DAG](https://github.com/ipfs/go-ipfs/blob/0cb22ccf359e05fb5b55a9bf2f9c515bf7d4dba7/merkledag/pb/merkledag.proto#L31-L39) +and [UnixFS Data](https://github.com/ipfs/go-ipfs/blob/0cb22ccf359e05fb5b55a9bf2f9c515bf7d4dba7/unixfs/pb/unixfs.proto#L3-L20) +of a file containing the "hello world" string. + +```json +{ + "Links": [], + "Data": "\u0008\u0002\u0012\u000bhello world\u0018\u000b" +} +``` + +`Data` is the protobuf encoding of the UnixFS Data. + +```json +{ + "Type": 2, + "Data": "aGVsbG8gd29ybGQ=", + "FileSize": 11, + "BlockSizes": null, + "HashType": null, + "Fanout": null +} +``` +### Chunked Format + +When the file's data exceeds the [chunking size](xref:Ipfs.CoreApi.AddFileOptions.ChunkSize), multiple [blocks](xref:Ipfs.CoreApi.IBlockApi) +are generated. The returned CID points to a block that has `Merkle.Links`. Each link +contains a chunk of the file. + +The following uses a chunking size of 6. A primary and two secondary blocks are created for "hello world". + +#### Primary Block + +```json +{ + "Links": [ + {"Name": "", "Hash": "QmPhmNbdBMtSQczNc4hnsMxRf5L4vfkU8jRTXDSHj8trSV", "Size": 14}, + {"Name": "", "Hash": "QmNyJpQkU1cEkBwMDhDNFstr42q55mqG5GE5Mgwug4xyGk", "Size": 13} + ], + "Data":"\u0008\u0002\u0018\u000b \u0006 \u0005" +} +{ + "Type": 2, + "Data": null, + "FileSize": 11, + "BlockSizes": [6,5], + "HashType":null, + "Fanout":null +} +``` + +#### First Link + +```json +{ + "Links": [], + "Data": "\u0008\u0002\u0012\u0006hello \u0018\u0006" +} +``` + +#### Second Link + +```json +{ + "Links": [], + "Data": "\u0008\u0002\u0012\u0005world\u0018\u0005" +} +``` + diff --git a/doc/articles/fs/raw.md b/doc/articles/fs/raw.md new file mode 100644 index 00000000..7012206d --- /dev/null +++ b/doc/articles/fs/raw.md @@ -0,0 +1,19 @@ +# Raw Leaves + +The [raw leaves option](xref:Ipfs.CoreApi.AddFileOptions.RawLeaves) specifies that +the file's data blocks are not [encapsulated](format.md) +with a Merkle DAG but simply contain the file's data. + +The [Cid.ContentType](xref:Ipfs.Cid.ContentType) is set to `raw`. + +```csharp +var options = new AddFileOptions +{ + RawLeaves = true +}; +var node = await ipfs.FileSystem.AddTextAsync("hello world", options); + +// zb2rhj7crUKTQYRGCRATFaQ6YFLTde2YzdqbbhAASkL9uRDXn +// base58btc cidv1 raw sha2-256 QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4 + +``` diff --git a/doc/articles/fs/wrap.md b/doc/articles/fs/wrap.md new file mode 100644 index 00000000..542e051a --- /dev/null +++ b/doc/articles/fs/wrap.md @@ -0,0 +1,34 @@ +# Wrapping + +The [wrap option](xref:Ipfs.CoreApi.AddFileOptions.Wrap) specifies that +the a directory is created for the file. + +```csharp +var path = "hello.txt"; +File.WriteAllText("hello.txt", "hello world"); +var options = new AddFileOptions +{ + Wrap = true +}; +var node = await ipfs.FileSystem.AddFileAsync(path, options); + +// QmNxvA5bwvPGgMXbmtyhxA1cKFdvQXnsGnZLCGor3AzYxJ + +``` +## Format + +Two blocks are created, a directory object and a file object. The file object +is described in [standard format](format.md). The directory object looks +like this. + +```json +{ + "Links": [ + {"Name": "hello.txt", "Hash": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", "Size":19} + ], + "Data": "\u0008\u0001" +} +{ + "Type": 1, +} +``` diff --git a/doc/articles/intro.md b/doc/articles/intro.md index 8a4bdd36..555cf948 100644 --- a/doc/articles/intro.md +++ b/doc/articles/intro.md @@ -15,7 +15,7 @@ package is published on [NuGet](https://www.nuget.org/packages/Ipfs.Engine). ## Related projects - [IPFS Core](https://github.com/richardschneider/net-ipfs-core) -- [IPFS API](https://github.com/richardschneider/net-ipfs-api) +- [IPFS HTTP Client](https://github.com/richardschneider/net-ipfs-http-client) ## Other implementations diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml index 2f946f82..14bde4ea 100644 --- a/doc/articles/toc.yml +++ b/doc/articles/toc.yml @@ -11,6 +11,17 @@ href: core-api.md - name: File System href: filesystem.md + items: + - name: CID only + href: fs/cid-only.md + - name: Encryption + href: fs/encryption.md + - name: Raw Leaves + href: fs/raw.md + - name: Wrapping + href: fs/wrap.md + - name: Format + href: fs/format.md - name: Repository href: repository.md items: diff --git a/src/BlockOptions.cs b/src/BlockOptions.cs index c837b639..39c45d29 100644 --- a/src/BlockOptions.cs +++ b/src/BlockOptions.cs @@ -33,5 +33,13 @@ public class BlockOptions /// Defaults to 64. /// public int InlineCidLimit { get; set; } = 64; + + /// + /// The maximun length of data block. + /// + /// + /// + /// 1MB (1024 * 1024) + public int MaxBlockSize { get; } = 1024 * 1024; } } diff --git a/src/CoreApi/BlockApi.cs b/src/CoreApi/BlockApi.cs index f6be7126..43523b03 100644 --- a/src/CoreApi/BlockApi.cs +++ b/src/CoreApi/BlockApi.cs @@ -163,6 +163,11 @@ public async Task PutAsync( bool pin = false, CancellationToken cancel = default(CancellationToken)) { + if (data.Length > ipfs.Options.Block.MaxBlockSize) + { + throw new ArgumentOutOfRangeException("data.Length", $"Block length can not exceed { ipfs.Options.Block.MaxBlockSize}."); + } + // Small enough for an inline CID? if (ipfs.Options.Block.AllowInlineCid && data.Length <= ipfs.Options.Block.InlineCidLimit) { diff --git a/src/CoreApi/FileSystemApi.cs b/src/CoreApi/FileSystemApi.cs index de2b6a86..37190cdd 100644 --- a/src/CoreApi/FileSystemApi.cs +++ b/src/CoreApi/FileSystemApi.cs @@ -57,10 +57,12 @@ public async Task AddAsync( // TODO: various options if (options.Trickle) throw new NotImplementedException("Trickle"); + var blockService = GetBlockService(options); + var keyChain = await ipfs.KeyChain(cancel); var chunker = new SizeChunker(); - var nodes = await chunker.ChunkAsync(stream, options, blockService, cancel); + var nodes = await chunker.ChunkAsync(stream, options, blockService, keyChain, cancel); // Multiple nodes for the file? FileSystemNode node = null; @@ -220,7 +222,8 @@ async Task CreateDirectoryAsync (IEnumerable li public async Task ReadFileAsync(string path, CancellationToken cancel = default(CancellationToken)) { var cid = await ipfs.ResolveIpfsPathToCidAsync(path, cancel); - return await FileSystem.CreateReadStream(cid, ipfs.Block, cancel); + var keyChain = await ipfs.KeyChain(cancel); + return await FileSystem.CreateReadStream(cid, ipfs.Block, keyChain, cancel); } public async Task ReadFileAsync(string path, long offset, long count = 0, CancellationToken cancel = default(CancellationToken)) diff --git a/src/Cryptography/Cms.cs b/src/Cryptography/Cms.cs new file mode 100644 index 00000000..2361d565 --- /dev/null +++ b/src/Cryptography/Cms.cs @@ -0,0 +1,165 @@ +using Org.BouncyCastle.Asn1.Cms; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Crypto; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ipfs.Engine.Cryptography +{ + public partial class KeyChain + { + /// + /// Encrypt data as CMS protected data. + /// + /// + /// The key name to protect the with. + /// + /// + /// The data to protect. + /// + /// + /// Is used to stop the task. When cancelled, the is raised. + /// + /// + /// A task that represents the asynchronous operation. The task's result is + /// the cipher text of the . + /// + /// + /// Cryptographic Message Syntax (CMS), aka PKCS #7 and + /// RFC 5652, + /// describes an encapsulation syntax for data protection. It + /// is used to digitally sign, digest, authenticate, and/or encrypt + /// arbitrary message content. + /// + public async Task CreateProtectedData( + string keyName, + byte[] plainText, + CancellationToken cancel = default(CancellationToken)) + { + // Identify the recipient by the Subject Key ID. + + // TODO: Need a method to just the get BC public key + // Get the BC key pair for the named key. + var ekey = await Store.TryGetAsync(keyName, cancel); + if (ekey == null) + throw new KeyNotFoundException($"The key '{keyName}' does not exist."); + AsymmetricCipherKeyPair kp = null; + UseEncryptedKey(ekey, key => + { + kp = this.GetKeyPairFromPrivateKey(key); + }); + + // Generate the protected data. + var edGen = new CmsEnvelopedDataGenerator(); + edGen.AddKeyTransRecipient(kp.Public, Base58.Decode(ekey.Id)); + var ed = edGen.Generate( + new CmsProcessableByteArray(plainText), + CmsEnvelopedDataGenerator.Aes256Cbc); + return ed.GetEncoded(); + } + + /// + /// Decrypt CMS protected data. + /// + /// + /// The protected CMS data. + /// + /// + /// Is used to stop the task. When cancelled, the is raised. + /// + /// + /// A task that represents the asynchronous operation. The task's result is + /// the plain text byte array of the protected data. + /// + /// + /// When the required private key, to decrypt the data, is not foumd. + /// + /// + /// Cryptographic Message Syntax (CMS), aka PKCS #7 and + /// RFC 5652, + /// describes an encapsulation syntax for data protection. It + /// is used to digitally sign, digest, authenticate, and/or encrypt + /// arbitrary message content. + /// + public async Task ReadProtectedData( + byte[] cipherText, + CancellationToken cancel = default(CancellationToken)) + { + var cms = new CmsEnvelopedDataParser(cipherText); + + // Find a recipient whose key we hold. We only deal with recipient names + // issued by ipfs (O=ipfs, OU=keystore). + var knownKeys = (await ListAsync(cancel)).ToArray(); + var recipient = cms + .GetRecipientInfos() + .GetRecipients() + .OfType() + .Select(ri => + { + var kid = GetKeyId(ri); + var key = knownKeys.FirstOrDefault(k => k.Id == kid); + return new { recipient = ri, key = key }; + }) + .FirstOrDefault(r => r.key != null); + if (recipient == null) + throw new KeyNotFoundException("The required decryption key is missing."); + + // Decrypt the contents. + var decryptionKey = await GetPrivateKeyAsync(recipient.key.Name); + return recipient.recipient.GetContent(decryptionKey); + } + + /// + /// Get the key ID for a recipient. + /// + /// + /// A recepient of the message. + /// + /// + /// The key ID of the recepient or null if the recepient info + /// is not understood or does not contain an IPFS key id. + /// + /// + /// The receipient inforomation has many formats; currently only + /// the key transport (ktri) form is supported. The key ID + /// is either the Subject Key Identifier (preferred) or the + /// issuer's distinguished name with the form "CN=<kid>,OU=keystore,O=ipfs". + /// + MultiHash GetKeyId(RecipientInformation ri) + { + // Any errors are simply ignored. + try + { + if (ri is KeyTransRecipientInformation ktri) + { + // Subject Key Identifier is the key ID. + if (ktri.RecipientID.SubjectKeyIdentifier is byte[] ski) + return new MultiHash(ski); + + // Issuer is CN=,OU=keystore,O=ipfs + var issuer = ktri.RecipientID.Issuer; + if (issuer != null + && issuer.GetValueList(X509Name.OU).Contains("keystore") + && issuer.GetValueList(X509Name.O).Contains("ipfs")) + { + var cn = issuer.GetValueList(X509Name.CN)[0] as string; + return new MultiHash(cn); + } + } + } + catch (Exception e) + { + log.Warn("Failed reading CMS recipient info.", e); + } + + return null; + } + + } +} diff --git a/src/Cryptography/KeyChain.cs b/src/Cryptography/KeyChain.cs index 989e5655..644c92f2 100644 --- a/src/Cryptography/KeyChain.cs +++ b/src/Cryptography/KeyChain.cs @@ -13,7 +13,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,7 +22,7 @@ namespace Ipfs.Engine.Cryptography /// /// A secure key chain. /// - public class KeyChain : Ipfs.CoreApi.IKeyApi + public partial class KeyChain : Ipfs.CoreApi.IKeyApi { static ILog log = LogManager.GetLogger(typeof(KeyChain)); @@ -157,6 +156,7 @@ FileStore Store /// public async Task GetPublicKeyAsync(string name, CancellationToken cancel = default(CancellationToken)) { + // TODO: Rename to GetIpfsPublicKeyAsync string result = null; var ekey = await Store.TryGetAsync(name, cancel); if (ekey != null) @@ -216,12 +216,8 @@ FileStore Store g.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); break; case "secp256k1": - X9ECParameters ecP = ECNamedCurveTable.GetByName(keyType); - if (ecP == null) - throw new Exception("unknown curve name: " + keyType); - var domain = new ECDomainParameters(ecP.Curve, ecP.G, ecP.N, ecP.H, ecP.GetSeed()); g = GeneratorUtilities.GetKeyPairGenerator("EC"); - g.Init(new ECKeyGenerationParameters(domain, new SecureRandom())); + g.Init(new ECKeyGenerationParameters(SecObjectIdentifiers.SecP256k1, new SecureRandom())); break; default: throw new Exception($"Invalid key type '{keyType}'."); @@ -435,7 +431,7 @@ AsymmetricCipherKeyPair GetKeyPairFromPrivateKey(AsymmetricKeyParameter privateK else if (privateKey is ECPrivateKeyParameters ec) { var q = ec.Parameters.G.Multiply(ec.D); - var pub = new ECPublicKeyParameters(q, ec.Parameters); + var pub = new ECPublicKeyParameters(ec.AlgorithmName, q, ec.PublicKeyParamSet); keyPair = new AsymmetricCipherKeyPair(pub, ec); } if (keyPair == null) diff --git a/src/Cryptography/Pki.cs b/src/Cryptography/Pki.cs new file mode 100644 index 00000000..ca5d7693 --- /dev/null +++ b/src/Cryptography/Pki.cs @@ -0,0 +1,111 @@ +using Org.BouncyCastle.Asn1.EdEC; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Operators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ipfs.Engine.Cryptography +{ + public partial class KeyChain + { + /// + /// Create a X509 certificate for the specified key. + /// + /// + /// The key name. + /// + /// + /// + public async Task CreateCertificateAsync( + string keyName, + CancellationToken cancel = default(CancellationToken)) + { + var cert = await CreateBCCertificateAsync(keyName, cancel); + return cert.GetEncoded(); + } + + /// + /// Create a X509 certificate for the specified key. + /// + /// + /// The key name. + /// + /// + /// + public async Task CreateBCCertificateAsync( + string keyName, + CancellationToken cancel = default(CancellationToken)) + { + // Get the BC key pair for the named key. + var ekey = await Store.TryGetAsync(keyName, cancel); + if (ekey == null) + throw new KeyNotFoundException($"The key '{keyName}' does not exist."); + AsymmetricCipherKeyPair kp = null; + UseEncryptedKey(ekey, key => + { + kp = this.GetKeyPairFromPrivateKey(key); + }); + + // A signer for the key. + var ku = new KeyUsage(KeyUsage.DigitalSignature + | KeyUsage.DataEncipherment + | KeyUsage.KeyEncipherment); + ISignatureFactory signatureFactory = null; + if (kp.Private is ECPrivateKeyParameters) + { + signatureFactory = new Asn1SignatureFactory( + X9ObjectIdentifiers.ECDsaWithSha256.ToString(), + kp.Private); + } + else if (kp.Private is RsaPrivateCrtKeyParameters) + { + signatureFactory = new Asn1SignatureFactory( + PkcsObjectIdentifiers.Sha256WithRsaEncryption.ToString(), + kp.Private); + } + else if (kp.Private is Ed25519PrivateKeyParameters) + { + signatureFactory = new Asn1SignatureFactory( + EdECObjectIdentifiers.id_Ed25519.Id.ToString(), + kp.Private); + ku = new KeyUsage(KeyUsage.DigitalSignature); + } + if (signatureFactory == null) + { + throw new NotSupportedException($"The key type {kp.Private.GetType().Name} is not supported."); + } + + // Build the certificate. + var dn = new X509Name($"CN={ekey.Id}, OU=keystore, O=ipfs"); + var ski = new SubjectKeyIdentifier(Base58.Decode(ekey.Id)); + // Not a certificate authority. + // TODO: perhaps the "self" key is a CA and all other keys issued by it. + var bc = new BasicConstraints(false); + + var certGenerator = new X509V3CertificateGenerator(); + certGenerator.SetIssuerDN(dn); + certGenerator.SetSubjectDN(dn); + certGenerator.SetSerialNumber(BigInteger.ValueOf(1)); + certGenerator.SetNotAfter(DateTime.UtcNow.AddYears(10)); + certGenerator.SetNotBefore(DateTime.UtcNow); + certGenerator.SetPublicKey(kp.Public); + certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, ski); + certGenerator.AddExtension(X509Extensions.BasicConstraints, true, bc); + certGenerator.AddExtension(X509Extensions.KeyUsage, false, ku); + + return certGenerator.Generate(signatureFactory); + } + + } +} diff --git a/src/IpfsEngine.csproj b/src/IpfsEngine.csproj index 9ba3a82a..adfdc8fb 100644 --- a/src/IpfsEngine.csproj +++ b/src/IpfsEngine.csproj @@ -39,12 +39,12 @@ - + - + diff --git a/src/UnixFileSystem/ChunkedStream.cs b/src/UnixFileSystem/ChunkedStream.cs index 2d6a9e04..c6ce1817 100644 --- a/src/UnixFileSystem/ChunkedStream.cs +++ b/src/UnixFileSystem/ChunkedStream.cs @@ -1,4 +1,5 @@ using Ipfs.CoreApi; +using Ipfs.Engine.Cryptography; using ProtoBuf; using System; using System.Collections.Generic; @@ -32,10 +33,12 @@ class BlockInfo /// the specified and . /// /// + /// /// - public ChunkedStream (IBlockApi blockService, DagNode dag) + public ChunkedStream (IBlockApi blockService, KeyChain keyChain, DagNode dag) { BlockService = blockService; + KeyChain = keyChain; var links = dag.Links.ToArray(); var dm = Serializer.Deserialize(dag.DataStream); fileSize = (long)dm.FileSize; @@ -52,6 +55,7 @@ public ChunkedStream (IBlockApi blockService, DagNode dag) } IBlockApi BlockService { get; set; } + KeyChain KeyChain { get; set; } /// public override long Length => fileSize; @@ -125,7 +129,7 @@ async Task> GetBlockAsync (long position, CancellationToken c var need = blocks.Last(b => b.Position <= position); if (need != currentBlock) { - var stream = await FileSystem.CreateReadStream(need.Id, BlockService, cancel); + var stream = await FileSystem.CreateReadStream(need.Id, BlockService, KeyChain, cancel); currentBlock = need; currentData = new byte[stream.Length]; for (int i = 0, n; i < stream.Length; i += n) diff --git a/src/UnixFileSystem/FileSystem.cs b/src/UnixFileSystem/FileSystem.cs index e295046c..ca80686a 100644 --- a/src/UnixFileSystem/FileSystem.cs +++ b/src/UnixFileSystem/FileSystem.cs @@ -1,4 +1,5 @@ using Ipfs.CoreApi; +using Ipfs.Engine.Cryptography; using ProtoBuf; using System; using System.Collections.Generic; @@ -24,7 +25,10 @@ public static class FileSystem /// The identifier of some content. /// /// - /// The source of cid's data. + /// The source of the cid's data. + /// + /// + /// Used to decypt the protected data blocks. /// /// /// Is used to stop the task. When cancelled, the is raised. @@ -37,16 +41,19 @@ public static class FileSystem /// The id's is used to determine how to read /// the conent. /// - public static async Task CreateReadStream( + public static Task CreateReadStream( Cid id, IBlockApi blockService, + KeyChain keyChain, CancellationToken cancel) { // TODO: A content-type registry should be used. if (id.ContentType == "dag-pb") - return await CreateDagProtoBufStreamAsync(id, blockService, cancel); + return CreateDagProtoBufStreamAsync(id, blockService, keyChain, cancel); else if (id.ContentType == "raw") - return await CreateRawStreamAsync(id, blockService, cancel); + return CreateRawStreamAsync(id, blockService, keyChain, cancel); + else if (id.ContentType == "cms") + return CreateCmsStreamAsync(id, blockService, keyChain, cancel); else throw new NotSupportedException($"Cannot read content type '{id.ContentType}'."); } @@ -54,6 +61,7 @@ public static async Task CreateReadStream( static async Task CreateRawStreamAsync( Cid id, IBlockApi blockService, + KeyChain keyChain, CancellationToken cancel) { var block = await blockService.GetAsync(id, cancel); @@ -63,6 +71,7 @@ static async Task CreateRawStreamAsync( static async Task CreateDagProtoBufStreamAsync( Cid id, IBlockApi blockService, + KeyChain keyChain, CancellationToken cancel) { var block = await blockService.GetAsync(id, cancel); @@ -82,10 +91,21 @@ static async Task CreateDagProtoBufStreamAsync( if (dm.BlockSizes != null) { - return new ChunkedStream(blockService, dag); + return new ChunkedStream(blockService, keyChain, dag); } throw new Exception($"Cannot determine the file format of '{id}'."); } + + static async Task CreateCmsStreamAsync( + Cid id, + IBlockApi blockService, + KeyChain keyChain, + CancellationToken cancel) + { + var block = await blockService.GetAsync(id, cancel); + var plain = await keyChain.ReadProtectedData(block.DataBytes, cancel); + return new MemoryStream(plain, false); + } } } diff --git a/src/UnixFileSystem/SizeChunker.cs b/src/UnixFileSystem/SizeChunker.cs index 262c3a36..9e136240 100644 --- a/src/UnixFileSystem/SizeChunker.cs +++ b/src/UnixFileSystem/SizeChunker.cs @@ -1,4 +1,5 @@ using Ipfs.CoreApi; +using Ipfs.Engine.Cryptography; using System; using System.Collections.Generic; using System.IO; @@ -26,6 +27,9 @@ public class SizeChunker /// /// The destination for the chunked data block(s). /// + /// + /// Used to protect the chunked data blocks(s). + /// /// /// Is used to stop the task. When cancelled, the is raised. /// @@ -36,11 +40,13 @@ public class SizeChunker public async Task> ChunkAsync( Stream stream, AddFileOptions options, - IBlockApi blockService, + IBlockApi blockService, + KeyChain keyChain, CancellationToken cancel) { + var protecting = !string.IsNullOrWhiteSpace(options.ProtectionKey); var nodes = new List (); - var chunkSize = options.ChunkSize; // TODO: Upper limit for DOS attacks. + var chunkSize = options.ChunkSize; var chunk = new byte[chunkSize]; var chunking = true; @@ -66,7 +72,29 @@ public async Task> ChunkAsync( break; } - if (options.RawLeaves) + // if protected data, then get CMS structure. + if (protecting) + { + // TODO: Inefficent to copy chunk, use ArraySegment in DataMessage.Data + var plain = new byte[length]; + Array.Copy(chunk, plain, length); + var cipher = await keyChain.CreateProtectedData(options.ProtectionKey, plain, cancel); + var cid = await blockService.PutAsync( + data: cipher, + contentType: "cms", + multiHash: options.Hash, + encoding: options.Encoding, + pin: options.Pin, + cancel: cancel); + nodes.Add(new FileSystemNode + { + Id = cid, + Size = length, + DagSize = cipher.Length, + Links = FileSystemLink.None + }); + } + else if (options.RawLeaves) { // TODO: Inefficent to copy chunk, use ArraySegment in DataMessage.Data var data = new byte[length]; diff --git a/test/CoreApi/BlockApiTest.cs b/test/CoreApi/BlockApiTest.cs index 93588c1c..fc014280 100644 --- a/test/CoreApi/BlockApiTest.cs +++ b/test/CoreApi/BlockApiTest.cs @@ -26,6 +26,16 @@ public void Put_Bytes() CollectionAssert.AreEqual(blob, data.DataBytes); } + [TestMethod] + public void Put_Bytes_TooBig() + { + var data = new byte[ipfs.Options.Block.MaxBlockSize + 1]; + ExceptionAssert.Throws(() => + { + var cid = ipfs.Block.PutAsync(data).Result; + }); + } + [TestMethod] public void Put_Bytes_ContentType() { diff --git a/test/CoreApi/FileSystemApiTest.cs b/test/CoreApi/FileSystemApiTest.cs index ac6ff1f7..6675b6aa 100644 --- a/test/CoreApi/FileSystemApiTest.cs +++ b/test/CoreApi/FileSystemApiTest.cs @@ -262,6 +262,40 @@ public async Task Add_RawAndChunked() Assert.AreEqual("hello world", text); } + [TestMethod] + public async Task Add_Protected() + { + var ipfs = TestFixture.Ipfs; + var options = new AddFileOptions + { + ProtectionKey = "self" + }; + var node = await ipfs.FileSystem.AddTextAsync("hello world", options); + Assert.AreEqual("cms", node.Id.ContentType); + Assert.AreEqual(0, node.Links.Count()); + Assert.AreEqual(false, node.IsDirectory); + + var text = await ipfs.FileSystem.ReadAllTextAsync(node.Id); + Assert.AreEqual("hello world", text); + } + + [TestMethod] + public async Task Add_Protected_Chunked() + { + var ipfs = TestFixture.Ipfs; + var options = new AddFileOptions + { + ProtectionKey = "self", + ChunkSize = 3 + }; + var node = await ipfs.FileSystem.AddTextAsync("hello world", options); + Assert.AreEqual(4, node.Links.Count()); + Assert.AreEqual(false, node.IsDirectory); + + var text = await ipfs.FileSystem.ReadAllTextAsync(node.Id); + Assert.AreEqual("hello world", text); + } + [TestMethod] public async Task Add_OnlyHash() { @@ -367,6 +401,82 @@ public async Task Read_ChunkedWithLength() } } + [TestMethod] + public async Task Read_ProtectedWithLength() + { + var text = "hello world"; + var ipfs = TestFixture.Ipfs; + var options = new AddFileOptions + { + ProtectionKey = "self" + }; + var node = await ipfs.FileSystem.AddTextAsync(text, options); + + for (var offset = 0; offset < text.Length; ++offset) + { + for (var length = text.Length + 1; 0 < length; --length) + { + using (var data = await ipfs.FileSystem.ReadFileAsync(node.Id, offset, length)) + using (var reader = new StreamReader(data)) + { + var s = reader.ReadToEnd(); + Assert.AreEqual(text.Substring(offset, Math.Min(11 - offset, length)), s, $"o={offset} l={length}"); + } + } + } + } + + [TestMethod] + public async Task Read_ProtectedChunkedWithLength() + { + var text = "hello world"; + var ipfs = TestFixture.Ipfs; + var options = new AddFileOptions + { + ChunkSize = 3, + ProtectionKey = "self" + }; + var node = await ipfs.FileSystem.AddTextAsync(text, options); + + for (var offset = 0; offset < text.Length; ++offset) + { + for (var length = text.Length + 1; 0 < length; --length) + { + using (var data = await ipfs.FileSystem.ReadFileAsync(node.Id, offset, length)) + using (var reader = new StreamReader(data)) + { + var s = reader.ReadToEnd(); + Assert.AreEqual(text.Substring(offset, Math.Min(11 - offset, length)), s, $"o={offset} l={length}"); + } + } + } + } + + [TestMethod] + public async Task Read_ProtectedMissingKey() + { + var text = "hello world"; + var ipfs = TestFixture.Ipfs; + var key = await ipfs.Key.CreateAsync("alice", "rsa", 512); + try + { + var options = new AddFileOptions { ProtectionKey = key.Name }; + var node = await ipfs.FileSystem.AddTextAsync(text, options); + Assert.AreEqual(text, await ipfs.FileSystem.ReadAllTextAsync(node.Id)); + + await ipfs.Key.RemoveAsync(key.Name); + ExceptionAssert.Throws(() => + { + var _ = ipfs.FileSystem.ReadAllTextAsync(node.Id).Result; + }); + } + finally + { + await ipfs.Key.RemoveAsync(key.Name); + } + + } + [TestMethod] public void AddDirectory() { diff --git a/test/CoreApi/KeyApiTest.cs b/test/CoreApi/KeyApiTest.cs index 641ce9e5..d63341d6 100644 --- a/test/CoreApi/KeyApiTest.cs +++ b/test/CoreApi/KeyApiTest.cs @@ -225,7 +225,7 @@ public async Task Import_OpenSSL_Bitcoin() await ipfs.Key.RemoveAsync("ob1"); var key = await ipfs.Key.ImportAsync("ob1", pem); Assert.AreEqual("ob1", key.Name); - Assert.AreEqual("QmdLk1BNDtW61fsyKyNkqjqzvW979TuC4urcP5Hb3P6qvq", key.Id); + Assert.AreEqual("QmUUYGCaT2eYDH8RT7dJSM9zMexZGEnf6fMUy6nD9C31xZ", key.Id); var keychain = await ipfs.KeyChain(); var privateKey = await keychain.GetPrivateKeyAsync("ob1"); diff --git a/test/Cryptography/CertTest.cs b/test/Cryptography/CertTest.cs new file mode 100644 index 00000000..16c9cf6c --- /dev/null +++ b/test/Cryptography/CertTest.cs @@ -0,0 +1,74 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.X509.Extension; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ipfs.Engine.Cryptography +{ + [TestClass] + public class CertTest + { + [TestMethod] + public async Task Create_Rsa() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "rsa", 512); + try + { + var cert = await keychain.CreateBCCertificateAsync(key.Name); + Assert.AreEqual($"CN={key.Id},OU=keystore,O=ipfs", cert.SubjectDN.ToString()); + var ski = new SubjectKeyIdentifierStructure(cert.GetExtensionValue(X509Extensions.SubjectKeyIdentifier)); + Assert.AreEqual(key.Id.ToBase58(), ski.GetKeyIdentifier().ToBase58()); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + [TestMethod] + public async Task Create_Secp256k1() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "secp256k1", 0); + try + { + var cert = await keychain.CreateBCCertificateAsync("alice"); + Assert.AreEqual($"CN={key.Id},OU=keystore,O=ipfs", cert.SubjectDN.ToString()); + var ski = new SubjectKeyIdentifierStructure(cert.GetExtensionValue(X509Extensions.SubjectKeyIdentifier)); + Assert.AreEqual(key.Id.ToBase58(), ski.GetKeyIdentifier().ToBase58()); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + [TestMethod] + public async Task Create_Ed25519() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "ed25519", 0); + try + { + var cert = await keychain.CreateBCCertificateAsync("alice"); + Assert.AreEqual($"CN={key.Id},OU=keystore,O=ipfs", cert.SubjectDN.ToString()); + var ski = new SubjectKeyIdentifierStructure(cert.GetExtensionValue(X509Extensions.SubjectKeyIdentifier)); + Assert.AreEqual(key.Id.ToBase58(), ski.GetKeyIdentifier().ToBase58()); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + } +} diff --git a/test/Cryptography/CmsTest.cs b/test/Cryptography/CmsTest.cs new file mode 100644 index 00000000..027f1cfb --- /dev/null +++ b/test/Cryptography/CmsTest.cs @@ -0,0 +1,146 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ipfs.Engine.Cryptography +{ + [TestClass] + public class CmsTest + { + [TestMethod] + public async Task ReadCms() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + string aliceKid = "QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA"; + string alice = @"-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA +MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG +QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd +1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7 +/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A +CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri +dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA +ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY +zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/ +ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt +0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83 +GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH +igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m +3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE +cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL +-----END ENCRYPTED PRIVATE KEY----- +"; + var key = await keychain.ImportAsync("alice", alice, "mypassword".ToArray()); + try + { + + Assert.AreEqual(aliceKid, key.Id); + + var cipher = Convert.FromBase64String(@" +MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK +EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI +WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B +AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k +d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO +knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3 +DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B +nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N +"); + var plain = await keychain.ReadProtectedData(cipher); + var plainText = Encoding.UTF8.GetString(plain); + Assert.AreEqual("This is a message from Alice to Bob", plainText); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + [TestMethod] + public async Task ReadCms_FailsWithoutKey() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var cipher = Convert.FromBase64String(@" +MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK +EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI +WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B +AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k +d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO +knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3 +DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B +nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N +"); + ExceptionAssert.Throws(() => + { + var plain = keychain.ReadProtectedData(cipher).Result; + }); + } + + [TestMethod] + public async Task CreateCms_Rsa() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "rsa", 512); + try + { + var data = new byte[] { 1, 2, 3, 4 }; + var cipher = await keychain.CreateProtectedData("alice", data); + var plain = await keychain.ReadProtectedData(cipher); + CollectionAssert.AreEqual(data, plain); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + [TestMethod] + [Ignore("NYI")] + public async Task CreateCms_Secp256k1() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "secp256k1", 0); + try + { + var data = new byte[] { 1, 2, 3, 4 }; + var cipher = await keychain.CreateProtectedData("alice", data); + File.WriteAllBytes(@"\tmp\secp256k1.cms", cipher); + var plain = await keychain.ReadProtectedData(cipher); + CollectionAssert.AreEqual(data, plain); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + [TestMethod] + [Ignore("NYI")] + public async Task CreateCms_Ed25519() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + var key = await ipfs.Key.CreateAsync("alice", "ed25519", 0); + try + { + var data = new byte[] { 1, 2, 3, 4 }; + var cipher = await keychain.CreateProtectedData("alice", data); + var plain = await keychain.ReadProtectedData(cipher); + CollectionAssert.AreEqual(data, plain); + } + finally + { + await ipfs.Key.RemoveAsync("alice"); + } + } + + } +} diff --git a/test/Cryptography/Rfc8410Test.cs b/test/Cryptography/Rfc8410Test.cs new file mode 100644 index 00000000..2bf07b1c --- /dev/null +++ b/test/Cryptography/Rfc8410Test.cs @@ -0,0 +1,70 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto.Parameters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ipfs.Engine.Cryptography +{ + [TestClass] + public class Rfc8410Test + { + [TestMethod] + public async Task ReadPrivateKey() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + string alice1 = @"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC +-----END PRIVATE KEY----- +"; + var key = await keychain.ImportAsync("alice1", alice1, null); + try + { + var priv = (Ed25519PrivateKeyParameters) await keychain.GetPrivateKeyAsync("alice1"); + Assert.IsTrue(priv.IsPrivate); + Assert.AreEqual("d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842", priv.GetEncoded().ToHexString()); + + var pub = priv.GeneratePublicKey(); + Assert.IsFalse(pub.IsPrivate); + Assert.AreEqual("19bf44096984cdfe8541bac167dc3b96c85086aa30b6b6cb0c5c38ad703166e1", pub.GetEncoded().ToHexString()); + } + finally + { + await ipfs.Key.RemoveAsync("alice1"); + } + } + + [TestMethod] + public async Task ReadPrivateAndPublicKey() + { + var ipfs = TestFixture.Ipfs; + var keychain = await ipfs.KeyChain(); + string alice1 = @"-----BEGIN PRIVATE KEY----- +MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC +oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB +Z9w7lshQhqowtrbLDFw4rXAxZuE= +-----END PRIVATE KEY----- +"; + var key = await keychain.ImportAsync("alice1", alice1, null); + try + { + var priv = (Ed25519PrivateKeyParameters)await keychain.GetPrivateKeyAsync("alice1"); + Assert.IsTrue(priv.IsPrivate); + Assert.AreEqual("d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842", priv.GetEncoded().ToHexString()); + + var pub = priv.GeneratePublicKey(); + Assert.IsFalse(pub.IsPrivate); + Assert.AreEqual("19bf44096984cdfe8541bac167dc3b96c85086aa30b6b6cb0c5c38ad703166e1", pub.GetEncoded().ToHexString()); + } + finally + { + await ipfs.Key.RemoveAsync("alice1"); + } + } + + } +}