Initial SDK structure #1

Merged
i.pchelintsev merged 18 commits from i.pchelintsev/frostfs-sdk-csharp:master into master 2024-09-04 19:51:24 +00:00
17 changed files with 182 additions and 99 deletions
Showing only changes of commit b307c2c899 - Show all commits

View file

@ -2,6 +2,7 @@ using System.Security.Cryptography;
using FrostFS.Container;
using FrostFS.Netmap;
using FrostFS.Object;
using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
using FrostFS.SDK.ModelsV2;

View file

@ -5,7 +5,7 @@ using DeleteResponse = FrostFS.Container.DeleteResponse;
using GetResponse = FrostFS.Container.GetResponse;
using PutResponse = FrostFS.Container.PutResponse;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.ClientV2.Interfaces;
public interface IFrostFSClient
{
@ -14,6 +14,7 @@ public interface IFrostFSClient
Task<GetResponse> GetContainerAsync(ContainerID containerId);
Task<DeleteResponse> DeleteContainerAsync(ContainerID containerId);
Task<HeadResponse> GetObjectHeadAsync(ContainerID containerId, ObjectID objectId);
Task<Object.PutResponse> PutObjectAsync(Object.Header header, Stream payload);
Task<Object.Object> GetObjectAsync(ContainerID containerId, ObjectID objectId);
Task<Object.PutResponse> PutObjectAsync(Header header, Stream payload);
Task<Object.DeleteResponse> DeleteObjectAsync(ContainerID containerId, ObjectID objectId);
}

View file

@ -19,14 +19,14 @@ public static class ContainerMapper
public static ModelsV2.Container ToModel(this Container.Container container)
{
var basicAclName = Enum.GetName(typeof(BasicACL), container.BasicAcl);
var basicAclName = Enum.GetName(typeof(BasicAcl), container.BasicAcl);
if (basicAclName is null)
{
throw new ArgumentException($"Unknown BasicACL rule. Value: '{container.BasicAcl}'.");
}
return new ModelsV2.Container(
Enum.Parse<BasicACL>(basicAclName),
Enum.Parse<BasicAcl>(basicAclName),
container.PlacementPolicy.ToModel()
)
{

View file

@ -24,12 +24,9 @@ public static class PlacementPolicyMapper
public static ModelsV2.Netmap.PlacementPolicy ToModel(this PlacementPolicy placementPolicy)
{
var replicas = new List<Replica>();
foreach (var replica in placementPolicy.Replicas)
{
replicas.Add(replica.ToModel());
}
return new ModelsV2.Netmap.PlacementPolicy(placementPolicy.Unique, replicas.ToArray());
return new ModelsV2.Netmap.PlacementPolicy(
placementPolicy.Unique,
placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
);
}
}

View file

@ -52,20 +52,27 @@ public static class ObjectHeadMapper
throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.");
}
var attributes = new List<ObjectAttribute>();
foreach (var attribute in header.Attributes)
{
attributes.Add(attribute.ToModel());
}
return new ObjectHeader(
ContainerId.FromHash(header.ContainerId.Value.ToByteArray()),
Enum.Parse<ModelsV2.Enums.ObjectType>(objTypeName),
attributes.ToArray()
header.Attributes.Select(attribute => attribute.ToModel()).ToArray()
)
{
Size = (long)header.PayloadLength,
Version = header.Version.ToModel()
};
}
}
public static class ObjectMapper
{
public static ModelsV2.Object ToModel(this Object.Object obj)
{
return new ModelsV2.Object
{
Header = obj.Header.ToModel(),
ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()),
Payload = obj.Payload.ToByteArray()
};
}
}

View file

@ -3,6 +3,7 @@ using FrostFS.Refs;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ModelsV2;
using FrostFS.Session;
using Version = FrostFS.SDK.ModelsV2.Version;
namespace FrostFS.SDK.ClientV2;

View file

@ -18,14 +18,14 @@ namespace FrostFS.SDK.ClientV2
public static byte[] SignRFC6979(this ECDsa key, byte[] data)
{
var digest = new Sha256Digest();
var secp256r1 = SecNamedCurves.GetByName("secp256r1");
var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N);
var private_key = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ec_parameters);
var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N);
var privateKey = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ecParameters);
var signer = new ECDsaSigner(new HMacDsaKCalculator(digest));
var hash = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, 0, data.Length);
digest.DoFinal(hash, 0);
signer.Init(true, private_key);
signer.Init(true, privateKey);
var rs = signer.GenerateSignature(hash);
var signature = new byte[RFC6979SignatureSize];
var rbytes = rs[0].ToByteArrayUnsigned();
@ -67,11 +67,11 @@ namespace FrostFS.SDK.ClientV2
public static Signature SignMessagePart(this ECDsa key, IMessage? data)
{
var data2sign = data is null ? Array.Empty<byte>() : data.ToByteArray();
var data2Sign = data is null ? Array.Empty<byte>() : data.ToByteArray();
var sig = new Signature
{
Key = ByteString.CopyFrom(key.PublicKey()),
Sign = ByteString.CopyFrom(key.SignData(data2sign)),
Sign = ByteString.CopyFrom(key.SignData(data2Sign)),
};
return sig;
}

View file

@ -24,19 +24,19 @@ namespace FrostFS.SDK.ClientV2 {
return rs;
}
public static bool VerifyRFC6979(this byte[] public_key, byte[] data, byte[] sig)
public static bool VerifyRFC6979(this byte[] publicKey, byte[] data, byte[] sig)
{
if (public_key is null || data is null || sig is null) return false;
if (publicKey is null || data is null || sig is null) return false;
var rs = DecodeSignature(sig);
var digest = new Sha256Digest();
var signer = new ECDsaSigner(new HMacDsaKCalculator(digest));
var secp256r1 = SecNamedCurves.GetByName("secp256r1");
var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N);
var bc_public_key = new ECPublicKeyParameters(secp256r1.Curve.DecodePoint(public_key), ec_parameters);
var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N);
var bcPublicKey = new ECPublicKeyParameters(secp256R1.Curve.DecodePoint(publicKey), ecParameters);
var hash = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, 0, data.Length);
digest.DoFinal(hash, 0);
signer.Init(false, bc_public_key);
signer.Init(false, bcPublicKey);
return signer.VerifySignature(hash, rs[0], rs[1]);
}
@ -54,8 +54,8 @@ namespace FrostFS.SDK.ClientV2 {
{
if (sig is null || sig.Key is null || sig.Sign is null) return false;
using var key = sig.Key.ToByteArray().LoadPublicKey();
var data2verify = data is null ? Array.Empty<byte>() : data.ToByteArray();
return key.VerifyData(data2verify, sig.Sign.ToByteArray());
var data2Verify = data is null ? Array.Empty<byte>() : data.ToByteArray();
return key.VerifyData(data2Verify, sig.Sign.ToByteArray());
}
public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification)
@ -65,8 +65,7 @@ namespace FrostFS.SDK.ClientV2 {
if (!verification.OriginSignature.VerifyMessagePart(origin)) return false;
if (origin is null)
return verification.BodySignature.VerifyMessagePart(body);
if (verification.BodySignature is not null) return false;
return VerifyMatryoskaLevel(body, meta.GetOrigin(), origin);
return verification.BodySignature is null && VerifyMatryoskaLevel(body, meta.GetOrigin(), origin);
}
public static bool Verify(this IVerificableMessage message)

View file

@ -65,25 +65,4 @@ public partial class Client
request.Sign(_key);
return await _containerServiceClient.DeleteAsync(request);
}
// private void PrepareContainerSessionToken(RequestMetaHeader meta, SessionToken sessionToken, ContainerID? cid,
// ContainerSessionContext.Types.Verb verb)
// {
// if (meta.SessionToken is not null) return;
// meta.SessionToken = sessionToken;
// var ctx = new ContainerSessionContext
// {
// Verb = verb
// };
// if (cid is null)
// {
// ctx.Wildcard = true;
// }
// else
// {
// ctx.ContainerId = cid;
// }
// meta.SessionToken.Body.Container = ctx;
// meta.SessionToken.Signature = _key.SignMessagePart(meta.SessionToken.Body);
// }
}

View file

@ -29,11 +29,64 @@ public partial class Client
return await _objectServiceClient.HeadAsync(request);
}
// public async Task<GetResponse> GetObjectAsync(ContainerID cid, ObjectID oid)
// {
//
// }
public async Task<Object.Object> GetObjectAsync(ContainerID cid, ObjectID oid)
{
var sessionToken = await CreateSessionAsync(uint.MaxValue);
var request = new GetRequest
{
Body = new GetRequest.Types.Body
{
Raw = false,
Address = new Address
{
ContainerId = cid,
ObjectId = oid
},
}
};
request.AddMetaHeader();
request.AddObjectSessionToken(
sessionToken,
cid,
oid,
ObjectSessionContext.Types.Verb.Get,
_key
);
request.Sign(_key);
return await GetObject(request);
}
private async Task<Object.Object> GetObject(GetRequest request)
{
using var stream = GetObjectInit(request);
var obj = await stream.ReadHeader();
var payload = new byte[obj.Header.PayloadLength];
var offset = 0;
var chunk = await stream.ReadChunk();
while (chunk is not null)
{
chunk.CopyTo(payload, offset);
offset += chunk.Length;
chunk = await stream.ReadChunk();
}
obj.Payload = ByteString.CopyFrom(payload);
return obj;
}
private ObjectReader GetObjectInit(GetRequest initRequest)
{
if (initRequest is null)
{
throw new ArgumentNullException(nameof(initRequest));
}
return new ObjectReader
{
Call = _objectServiceClient.Get(initRequest)
};
}
public async Task<PutResponse> PutObjectAsync(Header header, Stream payload)
{
var sessionToken = await CreateSessionAsync(uint.MaxValue);
@ -63,7 +116,7 @@ public partial class Client
);
request.Sign(_key);
using var stream = await InitObject(request);
using var stream = await PutObjectInit(request);
var buffer = new byte[Constants.ObjectChunkSize];
var bufferLength = payload.Read(buffer, 0, Constants.ObjectChunkSize);
while (bufferLength > 0)
@ -81,7 +134,7 @@ public partial class Client
return await stream.Close();
}
private async Task<ObjectStreamer> InitObject(PutRequest initRequest)
private async Task<ObjectStreamer> PutObjectInit(PutRequest initRequest)
{
if (initRequest is null)
{
@ -112,14 +165,47 @@ public partial class Client
}
}
internal class ObjectStreamer : IDisposable
internal class ObjectReader : IDisposable
{
public AsyncClientStreamingCall<PutRequest, PutResponse> Call { get; init; }
public AsyncServerStreamingCall<GetResponse> Call { get; init; }
public async Task<Object.Object> ReadHeader()
{
if (!await Call.ResponseStream.MoveNext())
{
throw new InvalidOperationException("unexpect end of stream");
}
var response = Call.ResponseStream.Current;
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init)
throw new InvalidOperationException("unexpect message type");
return new Object.Object
{
ObjectId = response.Body.Init.ObjectId,
Header = response.Body.Init.Header,
};
}
public async Task<byte[]?> ReadChunk()
{
if (!await Call.ResponseStream.MoveNext())
{
return null;
}
var response = Call.ResponseStream.Current;
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk)
throw new InvalidOperationException("unexpect message type");
return response.Body.Chunk.ToByteArray();
}
public void Dispose()
{
Call.Dispose();
}
}
internal class ObjectStreamer : IDisposable
{
public AsyncClientStreamingCall<PutRequest, PutResponse> Call { get; init; }
public async Task Write(PutRequest request)
{
@ -136,4 +222,9 @@ internal class ObjectStreamer : IDisposable
await Call.RequestStream.CompleteAsync();
return await Call.ResponseAsync;
}
}
public void Dispose()
{
Call.Dispose();
}
}

View file

@ -7,12 +7,11 @@ namespace FrostFS.SDK.Cryptography
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Concat(params byte[][] buffers)
{
int length = 0;
for (int i = 0; i < buffers.Length; i++)
length += buffers[i].Length;
byte[] dst = new byte[length];
int p = 0;
foreach (byte[] src in buffers)
var length = buffers.Sum(buffer => buffer.Length);
var dst = new byte[length];
var p = 0;
foreach (var src in buffers)
{
Buffer.BlockCopy(src, 0, dst, p, src.Length);
p += src.Length;

View file

@ -22,8 +22,8 @@ public static class KeyExtension
$"{nameof(Compress)} argument isn't uncompressed public key. " +
$"expected length={UncompressedPublicKeyLength}, actual={publicKey.Length}"
);
var secp256r1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256r1.Curve.DecodePoint(publicKey);
var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256R1.Curve.DecodePoint(publicKey);
return point.GetEncoded(true);
}
@ -34,8 +34,8 @@ public static class KeyExtension
$"{nameof(Decompress)} argument isn't compressed public key. " +
$"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}"
);
var secp256r1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256r1.Curve.DecodePoint(publicKey);
var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256R1.Curve.DecodePoint(publicKey);
return point.GetEncoded(false);
}
@ -115,19 +115,19 @@ public static class KeyExtension
return key.ExportParameters(true).D;
}
public static ECDsa LoadPrivateKey(this byte[] private_key)
public static ECDsa LoadPrivateKey(this byte[] privateKey)
{
var secp256r1 = SecNamedCurves.GetByName("secp256r1");
var public_key =
secp256r1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, private_key)).GetEncoded(false)[1..];
var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var publicKey =
secp256R1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, privateKey)).GetEncoded(false)[1..];
var key = ECDsa.Create(new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
D = private_key,
D = privateKey,
Q = new ECPoint
{
X = public_key[..32],
Y = public_key[32..]
X = publicKey[..32],
Y = publicKey[32..]
}
});
return key;
@ -135,20 +135,20 @@ public static class KeyExtension
public static ECDsa LoadWif(this string wif)
{
var private_key = GetPrivateKeyFromWIF(wif);
return LoadPrivateKey(private_key);
var privateKey = GetPrivateKeyFromWIF(wif);
return LoadPrivateKey(privateKey);
}
public static ECDsa LoadPublicKey(this byte[] public_key)
public static ECDsa LoadPublicKey(this byte[] publicKey)
{
var public_key_full = public_key.Decompress()[1..];
var publicKeyFull = publicKey.Decompress()[1..];
var key = ECDsa.Create(new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint
{
X = public_key_full[..32],
Y = public_key_full[32..]
X = publicKeyFull[..32],
Y = publicKeyFull[32..]
}
});
return key;

View file

@ -6,11 +6,11 @@ namespace FrostFS.SDK.ModelsV2;
public class Container
{
public Guid Nonce { get; set; }
public BasicACL BasicAcl { get; set; }
public BasicAcl BasicAcl { get; set; }
public PlacementPolicy PlacementPolicy { get; set; }
public Version Version { get; set; }
public Container(BasicACL basicAcl, PlacementPolicy placementPolicy)
public Container(BasicAcl basicAcl, PlacementPolicy placementPolicy)
{
Nonce = Guid.NewGuid();
BasicAcl = basicAcl;

View file

@ -4,7 +4,7 @@ namespace FrostFS.SDK.ModelsV2;
public class ContainerId
{
public string Value { get; }
public string Value { get; set; }
public ContainerId(string id)
{

View file

@ -2,7 +2,7 @@ using System.ComponentModel;
namespace FrostFS.SDK.ModelsV2.Enums;
public enum BasicACL
public enum BasicAcl
{
[Description("Basic ACL for private container")]
Private = 0x1C8C8CCC,

View file

@ -37,5 +37,6 @@ public class ObjectHeader
public class Object
{
public ObjectHeader Header { get; set; }
public Stream Payload { get; set; }
public ObjectId ObjectId { get; set; }
public byte[] Payload { get; set; }
}

View file

@ -1,4 +1,5 @@
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ModelsV2;
@ -16,14 +17,11 @@ public class FrostFsService
public async Task<ContainerId[]> ListContainersAsync()
{
var containersIds = new List<ContainerId>();
var listContainersResponse = await _client.ListContainersAsync();
foreach (var cid in listContainersResponse.Body.ContainerIds)
{
containersIds.Add(ContainerId.FromHash(cid.Value.ToByteArray()));
}
return containersIds.ToArray();
return listContainersResponse.Body.ContainerIds.Select(
cid => ContainerId.FromHash(cid.Value.ToByteArray())
).ToArray();
}
public async Task<ContainerId> CreateContainerAsync(ModelsV2.Container container)
@ -52,12 +50,21 @@ public class FrostFsService
return getObjectHeadResponse.Body.Header.Header.ToModel();
}
public async Task<ModelsV2.Object> GetObjectAsync(ContainerId containerId, ObjectId objectId)
{
var obj = await _client.GetObjectAsync(
containerId.ToGrpcMessage(),
objectId.ToGrpcMessage()
);
return obj.ToModel();
}
public async Task<ObjectId> PutObjectAsync(ObjectHeader header, Stream payload)
{
var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), payload);
return ObjectId.FromHash(putObjectResponse.Body.ObjectId.Value.ToByteArray());
}
public async Task<ObjectId> PutObjectAsync(ObjectHeader header, byte[] payload)
{
var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), new MemoryStream(payload));