Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mikhail Filimonov 2025-04-04 12:03:00 +04:00
commit 9052f0d32e
43 changed files with 1670 additions and 664 deletions

View File

@ -359,6 +359,7 @@ public struct PresentationGroupCallRequestedVideo: Equatable {
}
public var audioSsrc: UInt32
public var peerId: Int64
public var endpointId: String
public var ssrcGroups: [SsrcGroup]
public var minQuality: Quality
@ -383,7 +384,7 @@ public extension GroupCallParticipantsContext.Participant {
guard let videoDescription = self.videoDescription else {
return nil
}
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: self.peer.id.id._internalGetInt64Value(), endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in
PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs)
}, minQuality: minQuality, maxQuality: maxQuality)
}
@ -395,7 +396,7 @@ public extension GroupCallParticipantsContext.Participant {
guard let presentationDescription = self.presentationDescription else {
return nil
}
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: self.peer.id.id._internalGetInt64Value(), endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in
PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs)
}, minQuality: minQuality, maxQuality: maxQuality)
}

View File

@ -9904,9 +9904,10 @@ public extension Api.functions.phone {
}
}
public extension Api.functions.phone {
static func deleteConferenceCallParticipants(call: Api.InputGroupCall, ids: [Int64], block: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
static func deleteConferenceCallParticipants(flags: Int32, call: Api.InputGroupCall, ids: [Int64], block: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-115142380)
buffer.appendInt32(-1935276763)
serializeInt32(flags, buffer: buffer, boxed: false)
call.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(ids.count))
@ -9914,7 +9915,7 @@ public extension Api.functions.phone {
serializeInt64(item, buffer: buffer, boxed: false)
}
serializeBytes(block, buffer: buffer, boxed: false)
return (FunctionDescription(name: "phone.deleteConferenceCallParticipants", parameters: [("call", String(describing: call)), ("ids", String(describing: ids)), ("block", String(describing: block))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
return (FunctionDescription(name: "phone.deleteConferenceCallParticipants", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("ids", String(describing: ids)), ("block", String(describing: block))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {

View File

@ -752,8 +752,8 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS
return self.call.encrypt(message)
}
func decrypt(message: Data) -> Data? {
return self.call.decrypt(message)
func decrypt(message: Data, userId: Int64) -> Data? {
return self.call.decrypt(message, userId: userId)
}
}
@ -1240,13 +1240,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
engine: accountContext.engine,
callId: initialCall.description.id,
accessHash: initialCall.description.accessHash,
userId: accountContext.account.peerId.id._internalGetInt64Value(),
reference: initialCall.reference,
keyPair: keyPair,
initializeState: { keyPair, block in
initializeState: { keyPair, userId, block in
guard let keyPair = TdKeyPair(keyId: keyPair.id, publicKey: keyPair.publicKey.data) else {
return nil
}
guard let call = TdCall.make(with: keyPair, latestBlock: block) else {
guard let call = TdCall.make(with: keyPair, userId: userId, latestBlock: block) else {
return nil
}
return ConferenceCallE2EContextStateImpl(call: call)
@ -2103,8 +2104,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return self.e2eCall.with({ $0.state?.encrypt(message: message) })
}
func decrypt(message: Data) -> Data? {
return self.e2eCall.with({ $0.state?.decrypt(message: message) })
func decrypt(message: Data, userId: Int64) -> Data? {
return self.e2eCall.with({ $0.state?.decrypt(message: message, userId: userId) })
}
}
@ -3666,6 +3667,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
return OngoingGroupCallContext.VideoChannel(
audioSsrc: item.audioSsrc,
peerId: item.peerId,
endpointId: item.endpointId,
ssrcGroups: item.ssrcGroups.map { group in
return OngoingGroupCallContext.VideoChannel.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs)

View File

@ -13,7 +13,7 @@ public protocol ConferenceCallE2EContextState: AnyObject {
func takeOutgoingBroadcastBlocks() -> [Data]
func encrypt(message: Data) -> Data?
func decrypt(message: Data) -> Data?
func decrypt(message: Data, userId: Int64) -> Data?
}
public final class ConferenceCallE2EContext {
@ -31,9 +31,10 @@ public final class ConferenceCallE2EContext {
private let engine: TelegramEngine
private let callId: Int64
private let accessHash: Int64
private let userId: Int64
private let reference: InternalGroupCallReference
private let state: Atomic<ContextStateHolder>
private let initializeState: (TelegramKeyPair, Data) -> ConferenceCallE2EContextState?
private let initializeState: (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?
private let keyPair: TelegramKeyPair
let e2eEncryptionKeyHashValue = ValuePromise<Data?>(nil)
@ -52,7 +53,7 @@ public final class ConferenceCallE2EContext {
private var synchronizeRemovedParticipantsDisposable: Disposable?
private var synchronizeRemovedParticipantsTimer: Foundation.Timer?
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, reference: InternalGroupCallReference, state: Atomic<ContextStateHolder>, initializeState: @escaping (TelegramKeyPair, Data) -> ConferenceCallE2EContextState?, keyPair: TelegramKeyPair) {
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, state: Atomic<ContextStateHolder>, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?, keyPair: TelegramKeyPair) {
precondition(queue.isCurrent())
precondition(Queue.mainQueue().isCurrent())
@ -60,6 +61,7 @@ public final class ConferenceCallE2EContext {
self.engine = engine
self.callId = callId
self.accessHash = accessHash
self.userId = userId
self.reference = reference
self.state = state
self.initializeState = initializeState
@ -125,6 +127,7 @@ public final class ConferenceCallE2EContext {
private func addE2EBlocks(blocks: [Data], subChainId: Int) {
let keyPair = self.keyPair
let userId = self.userId
let initializeState = self.initializeState
let (outBlocks, outEmoji) = self.state.with({ callState -> ([Data], Data) in
if let state = callState.state {
@ -141,7 +144,7 @@ public final class ConferenceCallE2EContext {
guard let block = blocks.last else {
return ([], Data())
}
guard let state = initializeState(keyPair, block) else {
guard let state = initializeState(keyPair, userId, block) else {
return ([], Data())
}
callState.state = state
@ -286,7 +289,7 @@ public final class ConferenceCallE2EContext {
return .single(false)
}
return engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, participantIds: removedPeerIds, block: removeBlock)
return engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, mode: .cleanup, participantIds: removedPeerIds, block: removeBlock)
|> map { result -> Bool in
switch result {
case .success:
@ -321,11 +324,11 @@ public final class ConferenceCallE2EContext {
}
}
public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, reference: InternalGroupCallReference, keyPair: TelegramKeyPair, initializeState: @escaping (TelegramKeyPair, Data) -> ConferenceCallE2EContextState?) {
public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, keyPair: TelegramKeyPair, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?) {
let queue = Queue.mainQueue()
let state = self.state
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, engine: engine, callId: callId, accessHash: accessHash, reference: reference, state: state, initializeState: initializeState, keyPair: keyPair)
return Impl(queue: queue, engine: engine, callId: callId, accessHash: accessHash, userId: userId, reference: reference, state: state, initializeState: initializeState, keyPair: keyPair)
})
}

View File

@ -865,13 +865,25 @@ func _internal_inviteConferenceCallParticipant(account: Account, reference: Inte
}
}
public enum RemoveGroupCallBlockchainParticipantsMode {
case kick
case cleanup
}
public enum RemoveGroupCallBlockchainParticipantsResult {
case success
case pollBlocksAndRetry
}
func _internal_removeGroupCallBlockchainParticipants(account: Account, callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
return account.network.request(Api.functions.phone.deleteConferenceCallParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: participantIds, block: Buffer(data: block)))
func _internal_removeGroupCallBlockchainParticipants(account: Account, callId: Int64, accessHash: Int64, mode: RemoveGroupCallBlockchainParticipantsMode, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
var flags: Int32 = 0
switch mode {
case .kick:
flags |= 1 << 1
case .cleanup:
flags |= 1 << 0
}
return account.network.request(Api.functions.phone.deleteConferenceCallParticipants(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), ids: participantIds, block: Buffer(data: block)))
|> map { updates -> RemoveGroupCallBlockchainParticipantsResult in
account.stateManager.addUpdates(updates)
return .success

View File

@ -113,8 +113,8 @@ public extension TelegramEngine {
return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo)
}
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
return _internal_removeGroupCallBlockchainParticipants(account: self.account, callId: callId, accessHash: accessHash, participantIds: participantIds, block: block)
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, mode: RemoveGroupCallBlockchainParticipantsMode, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
return _internal_removeGroupCallBlockchainParticipants(account: self.account, callId: callId, accessHash: accessHash, mode: mode, participantIds: participantIds, block: block)
}
public func clearCachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal<Never, NoError> {

View File

@ -218,7 +218,7 @@ final class OngoingGroupCallBroadcastPartTaskImpl: NSObject, OngoingGroupCallBro
public protocol OngoingGroupCallEncryptionContext: AnyObject {
func encrypt(message: Data) -> Data?
func decrypt(message: Data) -> Data?
func decrypt(message: Data, userId: Int64) -> Data?
}
public final class OngoingGroupCallContext {
@ -300,13 +300,15 @@ public final class OngoingGroupCallContext {
}
public var audioSsrc: UInt32
public var peerId: Int64
public var endpointId: String
public var ssrcGroups: [SsrcGroup]
public var minQuality: Quality
public var maxQuality: Quality
public init(audioSsrc: UInt32, endpointId: String, ssrcGroups: [SsrcGroup], minQuality: Quality, maxQuality: Quality) {
public init(audioSsrc: UInt32, peerId: Int64, endpointId: String, ssrcGroups: [SsrcGroup], minQuality: Quality, maxQuality: Quality) {
self.audioSsrc = audioSsrc
self.peerId = peerId
self.endpointId = endpointId
self.ssrcGroups = ssrcGroups
self.minQuality = minQuality
@ -643,11 +645,11 @@ public final class OngoingGroupCallContext {
isConference: isConference,
isActiveByDefault: audioIsActiveByDefault,
encryptDecrypt: encryptionContext.flatMap { encryptionContext in
return { data, isEncrypt in
return { data, userId, isEncrypt in
if isEncrypt {
return encryptionContext.encrypt(message: data)
} else {
return encryptionContext.decrypt(message: data)
return encryptionContext.decrypt(message: data, userId: userId)
}
}
}
@ -897,6 +899,7 @@ public final class OngoingGroupCallContext {
}
return OngoingGroupCallRequestedVideoChannel(
audioSsrc: channel.audioSsrc,
userId: channel.peerId,
endpointId: channel.endpointId,
ssrcGroups: channel.ssrcGroups.map { group in
return OngoingGroupCallSsrcGroup(

View File

@ -402,13 +402,14 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) {
@interface OngoingGroupCallRequestedVideoChannel : NSObject
@property (nonatomic, readonly) uint32_t audioSsrc;
@property (nonatomic, readonly) int64_t userId;
@property (nonatomic, strong, readonly) NSString * _Nonnull endpointId;
@property (nonatomic, strong, readonly) NSArray<OngoingGroupCallSsrcGroup *> * _Nonnull ssrcGroups;
@property (nonatomic, readonly) OngoingGroupCallRequestedVideoQuality minQuality;
@property (nonatomic, readonly) OngoingGroupCallRequestedVideoQuality maxQuality;
- (instancetype _Nonnull)initWithAudioSsrc:(uint32_t)audioSsrc endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray<OngoingGroupCallSsrcGroup *> * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality;
- (instancetype _Nonnull)initWithAudioSsrc:(uint32_t)audioSsrc userId:(int64_t)userId endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray<OngoingGroupCallSsrcGroup *> * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality;
@end
@ -456,7 +457,7 @@ onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDet
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
isConference:(bool)isConference
isActiveByDefault:(bool)isActiveByDefault
encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryptDecrypt;
encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, int64_t, bool))encryptDecrypt;
- (void)stop:(void (^ _Nullable)())completion;

View File

@ -2371,14 +2371,14 @@ private:
enableNoiseSuppression:(bool)enableNoiseSuppression
disableAudioInput:(bool)disableAudioInput
enableSystemMute:(bool)enableSystemMute
prioritizeVP8:(bool)prioritizeVP8
prioritizeVP8:(bool)prioritizeVP8
logPath:(NSString * _Nonnull)logPath
statsLogPath:(NSString * _Nonnull)statsLogPath
onMutedSpeechActivityDetected:(void (^ _Nullable)(bool))onMutedSpeechActivityDetected
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice
isConference:(bool)isConference
isActiveByDefault:(bool)isActiveByDefault
encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryptDecrypt {
encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, int64_t, bool))encryptDecrypt {
self = [super init];
if (self != nil) {
_queue = queue;
@ -2447,12 +2447,12 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
std::string statsLogPathValue(statsLogPath.length == 0 ? "" : statsLogPath.UTF8String);
std::function<std::vector<uint8_t>(std::vector<uint8_t> const &, bool)> mappedEncryptDecrypt;
std::function<std::vector<uint8_t>(std::vector<uint8_t> const &, int64_t, bool)> mappedEncryptDecrypt;
if (encryptDecrypt) {
NSData * _Nullable (^encryptDecryptBlock)(NSData * _Nonnull, bool) = [encryptDecrypt copy];
mappedEncryptDecrypt = [encryptDecryptBlock](std::vector<uint8_t> const &message, bool isEncrypt) -> std::vector<uint8_t> {
NSData * _Nullable (^encryptDecryptBlock)(NSData * _Nonnull, int64_t, bool) = [encryptDecrypt copy];
mappedEncryptDecrypt = [encryptDecryptBlock](std::vector<uint8_t> const &message, int64_t userId, bool isEncrypt) -> std::vector<uint8_t> {
NSData *mappedMessage = [[NSData alloc] initWithBytes:message.data() length:message.size()];
NSData *result = encryptDecryptBlock(mappedMessage, isEncrypt);
NSData *result = encryptDecryptBlock(mappedMessage, userId, isEncrypt);
if (!result) {
return std::vector<uint8_t>();
}
@ -2635,6 +2635,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
continue;
}
}
mappedChannel.userId = channel.peerId;
mappedChannel.audioSsrc = channel.audioSsrc;
mappedChannel.videoInformation = channel.videoDescription.UTF8String ?: "";
mappedChannels.push_back(std::move(mappedChannel));
@ -2837,6 +2838,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
for (OngoingGroupCallRequestedVideoChannel *channel : requestedVideoChannels) {
tgcalls::VideoChannelDescription description;
description.audioSsrc = channel.audioSsrc;
description.userId = channel.userId;
description.endpointId = channel.endpointId.UTF8String ?: "";
for (OngoingGroupCallSsrcGroup *group in channel.ssrcGroups) {
tgcalls::MediaSsrcGroup parsedGroup;
@ -3075,10 +3077,11 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
@implementation OngoingGroupCallRequestedVideoChannel
- (instancetype)initWithAudioSsrc:(uint32_t)audioSsrc endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray<OngoingGroupCallSsrcGroup *> * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality {
- (instancetype)initWithAudioSsrc:(uint32_t)audioSsrc userId:(int64_t)userId endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray<OngoingGroupCallSsrcGroup *> * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality {
self = [super init];
if (self != nil) {
_audioSsrc = audioSsrc;
_userId = userId;
_endpointId = endpointId;
_ssrcGroups = ssrcGroups;
_minQuality = minQuality;

@ -1 +1 @@
Subproject commit 18ef54fd10115ad7fe73585b7bf8a7ddbe527124
Subproject commit fd1cfbd8151b2c32d5471a4f5431faa6274ce421

View File

@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface TdCall : NSObject
+ (nullable instancetype)makeWithKeyPair:(TdKeyPair *)keyPair latestBlock:(NSData *)latestBlock;
+ (nullable instancetype)makeWithKeyPair:(TdKeyPair *)keyPair userId:(int64_t)userId latestBlock:(NSData *)latestBlock;
- (NSArray<NSData *> *)takeOutgoingBroadcastBlocks;
- (NSData *)emojiState;
@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray<NSNumber *> *)participantIds;
- (nullable NSData *)encrypt:(NSData *)message;
- (nullable NSData *)decrypt:(NSData *)message;
- (nullable NSData *)decrypt:(NSData *)message userId:(int64_t)userId;
@end

View File

@ -91,7 +91,7 @@ static NSString *hexStringFromData(NSData *data) {
tde2e_api::call_destroy(_callId);
}
+ (nullable instancetype)makeWithKeyPair:(TdKeyPair *)keyPair latestBlock:(NSData *)latestBlock {
+ (nullable instancetype)makeWithKeyPair:(TdKeyPair *)keyPair userId:(int64_t)userId latestBlock:(NSData *)latestBlock {
std::string mappedLatestBlock((uint8_t *)latestBlock.bytes, ((uint8_t *)latestBlock.bytes) + latestBlock.length);
#if DEBUG
auto describeResult = tde2e_api::call_describe_block(mappedLatestBlock);
@ -112,7 +112,7 @@ static NSString *hexStringFromData(NSData *data) {
}
#endif
auto call = tde2e_api::call_create(keyPair.keyId, mappedLatestBlock);
auto call = tde2e_api::call_create(userId, keyPair.keyId, mappedLatestBlock);
if (!call.is_ok()) {
return nil;
}
@ -232,16 +232,16 @@ static NSString *hexStringFromData(NSData *data) {
- (nullable NSData *)encrypt:(NSData *)message {
std::string mappedMessage((uint8_t *)message.bytes, ((uint8_t *)message.bytes) + message.length);
auto result = tde2e_api::call_encrypt(_callId, mappedMessage);
auto result = tde2e_api::call_encrypt(_callId, 0, mappedMessage);
if (!result.is_ok()) {
return nil;
}
return [[NSData alloc] initWithBytes:result.value().data() length:result.value().size()];
}
- (nullable NSData *)decrypt:(NSData *)message {
- (nullable NSData *)decrypt:(NSData *)message userId:(int64_t)userId {
std::string mappedMessage((uint8_t *)message.bytes, ((uint8_t *)message.bytes) + message.length);
auto result = tde2e_api::call_decrypt(_callId, mappedMessage);
auto result = tde2e_api::call_decrypt(_callId, userId, 0, mappedMessage);
if (!result.is_ok()) {
return nil;
}

View File

@ -127,7 +127,7 @@ class HmacSha256ShortBench final : public td::Benchmark {
void run(int n) final {
unsigned char md[32];
for (int i = 0; i < n; i++) {
td::hmac_sha256(td::Slice(data, SHORT_DATA_SIZE), td::Slice(data, SHORT_DATA_SIZE), td::MutableSlice(md, 32));
td::hmac_sha256(td::Slice(data, 32), td::Slice(data, SHORT_DATA_SIZE), td::MutableSlice(md, 32));
}
}
};
@ -145,9 +145,9 @@ class HmacSha512ShortBench final : public td::Benchmark {
}
void run(int n) final {
unsigned char md[32];
unsigned char md[64];
for (int i = 0; i < n; i++) {
td::hmac_sha256(td::Slice(data, SHORT_DATA_SIZE), td::Slice(data, SHORT_DATA_SIZE), td::MutableSlice(md, 32));
td::hmac_sha512(td::Slice(data, 32), td::Slice(data, SHORT_DATA_SIZE), td::MutableSlice(md, 64));
}
}
};
@ -491,6 +491,11 @@ class Crc64Bench final : public td::Benchmark {
int main() {
td::init_openssl_threads();
td::bench(HmacSha256ShortBench());
td::bench(HmacSha512ShortBench());
td::bench(SHA1ShortBench());
td::bench(SHA256ShortBench());
td::bench(SHA512ShortBench());
td::bench(AesCtrBench());
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
td::bench(AesCtrOpenSSLBench());
@ -516,11 +521,6 @@ int main() {
#if OPENSSL_VERSION_NUMBER <= 0x10100000L
td::bench(SHA1Bench());
#endif
td::bench(SHA1ShortBench());
td::bench(SHA256ShortBench());
td::bench(SHA512ShortBench());
td::bench(HmacSha256ShortBench());
td::bench(HmacSha512ShortBench());
td::bench(Crc32Bench());
td::bench(Crc64Bench());
}

View File

@ -64,15 +64,10 @@ e2e.keyContactByPublicKey public_key:int256 = e2e.Key;
// TODO: store string instead of e2e.personalOnClient to support forward compatibility
e2e.valueContactByPublicKey entries:vector<e2e.personalOnClient> = e2e.Value;
//
// Blockchain
//
e2e.chain.groupBroadcastNonceCommit#d1512ae7 signature:int512 user_id:int64 chain_height:int32 chain_hash:int256 nonce_hash:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupBroadcastNonceReveal#83f4f9d8 signature:int512 user_id:int64 chain_height:int32 chain_hash:int256 nonce:int256 = e2e.chain.GroupBroadcast;
// TODO: this relies on fact that key is one time. It is critical
e2e.chain.groupBroadcastNonceCommit signature:int512 public_key:int256 chain_height:int32 nonce_hash:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupBroadcastNonceReveal signature:int512 public_key:int256 chain_height:int32 nonce:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true = e2e.chain.GroupParticipant;
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true version:int = e2e.chain.GroupParticipant;
e2e.chain.groupState participants:vector<e2e.chain.GroupParticipant> external_permissions:int = e2e.chain.GroupState;
e2e.chain.sharedKey ek:int256 encrypted_shared_key:string dest_user_id:vector<long> dest_header:vector<bytes> = e2e.chain.SharedKey;
@ -84,8 +79,7 @@ e2e.chain.changeSetSharedKey shared_key:e2e.chain.SharedKey = e2e.chain.Change;
e2e.chain.stateProof flags:# kv_hash:int256 group_state:flags.0?e2e.chain.GroupState shared_key:flags.1?e2e.chain.SharedKey = e2e.chain.StateProof;
// signature is for the same block, but with empty signature
// TODO: change flags
e2e.chain.block signature:int512 flags:# prev_block_hash:int256 changes:vector<e2e.chain.Change> height:int state_proof:e2e.chain.StateProof signature_public_key:flags.0?int256 = e2e.chain.Block;
e2e.chain.block#639a3db6 signature:int512 flags:# prev_block_hash:int256 changes:vector<e2e.chain.Change> height:int state_proof:e2e.chain.StateProof signature_public_key:flags.0?int256 = e2e.chain.Block;
--- functions ---

View File

@ -56,6 +56,7 @@ set(TDE2E_TEST_SOURCE
${CMAKE_CURRENT_SOURCE_DIR}/td/e2e/TestBlockchain.h
${CMAKE_CURRENT_SOURCE_DIR}/test/e2e.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/blockchain.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/encryption.cpp
)
set(TDE2E_TEST_SOURCE "${TDE2E_TEST_SOURCE}" PARENT_SCOPE)

View File

@ -20,6 +20,7 @@
#include "td/utils/tl_helpers.h"
#include "td/utils/tl_parsers.h"
#include <algorithm>
#include <map>
#include <set>
#include <tuple>
@ -28,12 +29,23 @@
namespace tde2e_core {
GroupParticipant GroupParticipant::from_tl(const td::e2e_api::e2e_chain_groupParticipant &participant) {
return GroupParticipant{participant.user_id_, participant.flags_, PublicKey::from_u256(participant.public_key_)};
return GroupParticipant{participant.user_id_, participant.flags_, PublicKey::from_u256(participant.public_key_),
participant.version_};
}
e2e::object_ptr<e2e::e2e_chain_groupParticipant> GroupParticipant::to_tl() const {
return e2e::make_object<e2e::e2e_chain_groupParticipant>(user_id, public_key.to_u256(), flags, add_users(),
remove_users());
remove_users(), version);
}
td::int32 GroupState::version() const {
if (participants.empty()) {
return 0;
}
td::int32 version = participants.front().version;
for (auto &participant : participants) {
version = std::min(version, participant.version);
}
return std::clamp(version, 0, 255);
}
td::Result<GroupParticipant> GroupState::get_participant(td::int64 user_id) const {
for (const auto &participant : participants) {
@ -52,12 +64,13 @@ td::Result<GroupParticipant> GroupState::get_participant(const PublicKey &public
}
return td::Status::Error("Participant not found");
};
Permissions GroupState::get_permissions(const PublicKey &public_key) const {
Permissions GroupState::get_permissions(const PublicKey &public_key, td::int32 limit_permissions) const {
limit_permissions &= GroupParticipantFlags::AllPermissions;
auto r_participant = get_participant(public_key);
if (r_participant.is_ok()) {
return Permissions{r_participant.ok().flags | GroupParticipantFlags::IsParticipant};
return Permissions{(r_participant.ok().flags & limit_permissions) | GroupParticipantFlags::IsParticipant};
}
return Permissions{external_permissions};
return Permissions{(external_permissions & limit_permissions)};
}
GroupStateRef GroupState::from_tl(const td::e2e_api::e2e_chain_groupState &state) {
@ -74,7 +87,7 @@ e2e::object_ptr<e2e::e2e_chain_groupState> GroupState::to_tl() const {
external_permissions);
}
GroupStateRef GroupState::empty_state() {
static GroupStateRef state = std::make_shared<GroupState>(GroupState{});
static GroupStateRef state = std::make_shared<GroupState>();
return state;
}
GroupSharedKeyRef GroupSharedKey::from_tl(const td::e2e_api::e2e_chain_sharedKey &shared_key) {
@ -87,7 +100,7 @@ e2e::object_ptr<e2e::e2e_chain_sharedKey> GroupSharedKey::to_tl() const {
std::vector<td::int64>(dest_user_id), std::vector(dest_header));
}
GroupSharedKeyRef GroupSharedKey::empty_shared_key() {
static GroupSharedKeyRef shared_key = std::make_shared<GroupSharedKey>(GroupSharedKey{});
static GroupSharedKeyRef shared_key = std::make_shared<GroupSharedKey>();
return shared_key;
}
ChangeSetValue ChangeSetValue::from_tl(const td::e2e_api::e2e_chain_changeSetValue &change) {
@ -166,7 +179,7 @@ td::Result<Block> Block::from_tl_serialized(td::Slice new_block) {
auto magic = parser.fetch_int();
if (magic != td::e2e_api::e2e_chain_block::ID) {
return td::Status::Error(PSLICE() << "Expected magic " << td::format::as_hex(td::e2e_api::e2e_chain_block::ID)
<< td::format::as_hex(magic));
<< ", got " << td::format::as_hex(magic));
}
auto block_tl = td::e2e_api::e2e_chain_block::fetch(parser);
parser.fetch_end();
@ -198,10 +211,18 @@ td::StringBuilder &operator<<(td::StringBuilder &sb, const Block &block) {
<< "\tchanges=" << block.changes_ << "\n"
<< "\tsignature_key=" << block.o_signature_public_key_ << ")";
}
td::Result<BitString> key_to_bitstring(td::Slice key) {
if (key.size() != 32) {
return td::Status::Error("Invalid key size");
}
return BitString(key);
}
td::Result<std::string> KeyValueState::get_value(td::Slice key) const {
return get(node_, BitString(key), snapshot_.value());
TRY_RESULT(bitstring, key_to_bitstring(key));
return get(node_, bitstring, snapshot_.value());
}
td::Result<std::string> KeyValueState::gen_proof(td::Span<td::Slice> keys) const {
// TODO: validate keys..
TRY_RESULT(pruned_tree, generate_pruned_tree(node_, keys, snapshot_.value()));
return TrieNode::serialize_for_network(pruned_tree);
}
@ -218,7 +239,8 @@ td::Result<std::string> KeyValueState::build_snapshot() const {
}
td::Status KeyValueState::set_value(td::Slice key, td::Slice value) {
TRY_RESULT_ASSIGN(node_, set(node_, BitString(key), value, snapshot_.value()));
TRY_RESULT(bitstring, key_to_bitstring(key));
TRY_RESULT_ASSIGN(node_, set(node_, bitstring, value, snapshot_.value()));
return td::Status::OK();
}
@ -277,55 +299,75 @@ td::Status State::set_value(td::Slice key, td::Slice value, const Permissions &p
return key_value_state_.set_value(key, value);
}
td::Status State::set_value_fast(KeyValueHash key_value_hash) {
td::Status State::set_value_fast(const KeyValueHash &key_value_hash) {
TRY_RESULT_ASSIGN(key_value_state_, KeyValueState::create_from_hash(key_value_hash));
return td::Status::OK();
}
td::Status State::set_group_state(GroupStateRef group_state, const Permissions &permissions) {
std::map<td::int64, td::int32> old_participants;
std::set<td::int64> new_participants;
td::Status State::validate_group_state(const GroupStateRef &group_state) {
std::set<td::int64> new_user_ids;
std::set<PublicKey> new_keys;
for (const auto &p : group_state_->participants) {
old_participants[p.user_id] = p.flags;
}
for (const auto &p : group_state->participants) {
new_participants.insert(p.user_id);
new_user_ids.insert(p.user_id);
new_keys.insert(p.public_key);
if ((p.flags & ~GroupParticipantFlags::AllPermissions) != 0) {
return Error(E::InvalidBlock_InvalidGroupState, "invalid permissions");
}
}
if (new_participants.size() != group_state->participants.size()) {
if ((group_state->external_permissions & ~GroupParticipantFlags::AllPermissions) != 0) {
return Error(E::InvalidBlock_InvalidGroupState, "invalid external permissions");
}
if (new_user_ids.size() != group_state->participants.size()) {
return Error(E::InvalidBlock_InvalidGroupState, "duplicate user_id");
}
if (new_keys.size() != group_state->participants.size()) {
return Error(E::InvalidBlock_InvalidGroupState, "duplicate public_key");
}
return td::Status::OK();
}
td::Status State::set_group_state(GroupStateRef group_state, const Permissions &permissions) {
TRY_STATUS(validate_group_state(group_state));
std::map<std::pair<td::int64, PublicKey>, td::int32> old_participants;
std::map<std::pair<td::int64, PublicKey>, td::int32> new_participants;
for (const auto &p : group_state_->participants) {
old_participants[std::make_pair(p.user_id, p.public_key)] = p.flags;
}
for (const auto &p : group_state->participants) {
new_participants[std::make_pair(p.user_id, p.public_key)] = p.flags;
}
if ((~group_state_->external_permissions & group_state->external_permissions) != 0) {
return Error(E::InvalidBlock_NoPermissions, "Can't increase external permissions");
}
td::int32 needed_flags = 0;
for (const auto &p : group_state_->participants) {
if (!new_participants.count(p.user_id)) {
for (const auto &[p, flags] : old_participants) {
if (!new_participants.count(p)) {
if (!permissions.may_remove_users()) {
return Error(E::InvalidBlock_NoPermissions, "Can't remove users");
}
}
}
for (const auto &p : group_state->participants) {
auto old_p = old_participants.find(p.user_id);
for (const auto &[p, flags] : new_participants) {
auto old_p = old_participants.find(p);
if (old_p == old_participants.end()) {
if (!permissions.may_add_users()) {
return Error(E::InvalidBlock_NoPermissions, "Can't add users");
}
needed_flags |= p.flags;
} else {
needed_flags |= p.flags & ~old_p->second;
needed_flags |= flags;
} else if (flags != old_p->second) {
if (!permissions.may_add_users() || !permissions.may_remove_users()) {
return Error(E::InvalidBlock_NoPermissions, "Can't add users");
}
needed_flags |= flags & ~old_p->second;
}
}
td::int32 missing_flags = needed_flags & ~permissions.flags;
td::int32 missing_flags = needed_flags & ~(permissions.flags & GroupParticipantFlags::AllPermissions);
if (missing_flags != 0) {
return Error(E::InvalidBlock_NoPermissions, "Can't give more permissions that we have");
return Error(E::InvalidBlock_NoPermissions, "Can't give more permissions than we have");
}
group_state_ = std::move(group_state);
return td::Status::OK();
@ -339,6 +381,31 @@ td::Status State::clear_shared_key(const Permissions &permissions) {
return td::Status::OK();
}
td::Status State::validate_shared_key(const GroupSharedKeyRef &shared_key, const GroupStateRef &group_state) {
if (shared_key->empty_shared_key()) {
return td::Status::OK();
}
if (shared_key->dest_user_id.size() != shared_key->dest_header.size()) {
return td::Status::Error("Shared key different number of users and headers");
}
if (shared_key->dest_user_id.size() != group_state->participants.size()) {
return td::Status::Error("Shared key has wrong number of users");
}
std::set<td::int64> participants;
for (const auto user_id : shared_key->dest_user_id) {
participants.insert(user_id);
}
if (participants.size() != shared_key->dest_user_id.size()) {
return td::Status::Error("Shared key has duplicated users");
}
for (auto &p : group_state->participants) {
if (!participants.count(p.user_id)) {
return td::Status::Error("Unknown user_id in SetSharedKey");
}
}
return td::Status::OK();
}
td::Status State::set_shared_key(GroupSharedKeyRef shared_key, const Permissions &permissions) {
if (*shared_key_ != *GroupSharedKey::empty_shared_key()) {
return td::Status::Error("Shared key is already set");
@ -346,16 +413,8 @@ td::Status State::set_shared_key(GroupSharedKeyRef shared_key, const Permissions
if (!permissions.may_change_shared_key()) {
return Error(E::InvalidBlock_NoPermissions, "Can't set shared key");
}
TRY_STATUS(validate_shared_key(shared_key, group_state_));
shared_key_ = std::move(shared_key);
std::set<td::int64> participants;
for (const auto &p : group_state_->participants) {
participants.insert(p.user_id);
}
for (auto dest_user_id : shared_key_->dest_user_id) {
if (!participants.count(dest_user_id)) {
return td::Status::Error("Unknown user_id in SetSharedKey");
}
}
return td::Status::OK();
}
@ -364,6 +423,9 @@ td::Status State::validate_state(const StateProof &state_proof) const {
return td::Status::Error("State hash mismatch");
}
if (!has_group_state_change_ && !has_set_value_) {
return Error(E::InvalidBlock_NoChanges, "There must be at least SetValue or SetGroupState changes");
}
if (has_group_state_change_ && state_proof.o_group_state) {
return Error(E::InvalidBlock_InvalidStateProof_Group,
"Group state must be omitted when there is a group state change");
@ -387,30 +449,40 @@ td::Status State::validate_state(const StateProof &state_proof) const {
return Error(E::InvalidBlock_InvalidStateProof_Secret, "shared key state differs");
}
TRY_STATUS(validate_group_state(group_state_));
TRY_STATUS(validate_shared_key(shared_key_, group_state_));
return td::Status::OK();
}
td::Status State::apply_change(const Change &change_outer, const PublicKey &public_key, bool full_apply) {
return std::visit(td::overloaded([](const ChangeNoop &change) { return td::Status::OK(); },
[this, full_apply, &public_key](const ChangeSetValue &change) {
if (full_apply) {
return set_value(change.key, change.value, group_state_->get_permissions(public_key));
}
return td::Status::OK();
},
[this, &public_key](const ChangeSetGroupState &change) {
has_group_state_change_ = true;
TRY_STATUS(set_group_state(change.group_state, group_state_->get_permissions(public_key)));
return clear_shared_key(group_state_->get_permissions(public_key));
},
[this, &public_key](const ChangeSetSharedKey &change) {
has_shared_key_change_ = true;
return set_shared_key(change.shared_key, group_state_->get_permissions(public_key));
}),
change_outer.value);
td::Status State::apply_change(const Change &change_outer, const PublicKey &public_key,
const ValidateOptions &validate_options) {
bool full_apply = validate_options.validate_state_hash;
auto limit_permissions = validate_options.permissions;
return std::visit(
td::overloaded(
[](const ChangeNoop &change) { return td::Status::OK(); },
[this, full_apply, limit_permissions, &public_key](const ChangeSetValue &change) {
has_set_value_ = true;
if (full_apply) {
return set_value(change.key, change.value, group_state_->get_permissions(public_key, limit_permissions));
}
return td::Status::OK();
},
[this, limit_permissions, &public_key](const ChangeSetGroupState &change) {
has_group_state_change_ = true;
TRY_STATUS(
set_group_state(change.group_state, group_state_->get_permissions(public_key, limit_permissions)));
return clear_shared_key(group_state_->get_permissions(public_key, limit_permissions));
},
[this, limit_permissions, &public_key](const ChangeSetSharedKey &change) {
has_shared_key_change_ = true;
return set_shared_key(change.shared_key, group_state_->get_permissions(public_key, limit_permissions));
}),
change_outer.value);
}
td::Status State::apply(Block &block, bool validate_state_hash) {
td::Status State::apply(Block &block, ValidateOptions validate_options) {
// To apply the first block an ephemeral -1 block is used
// - It has only one participant - Participant(user_id = 0, public_key = signer_public_key, permissions = all)
if (block.height_ == 0) {
@ -426,22 +498,21 @@ td::Status State::apply(Block &block, bool validate_state_hash) {
return td::Status::Error("Unknown public key");
}
// 3. Would identify permissions of the participant who created the block, i.e. the one with `signer_public_key` public key.
// - First we look for signer public key in the previous state. If found use its permissions
// - Otherwise, we use external_permissions GroupParticipant participant;
// 5. Verifies the signature of the block.
TRY_STATUS(block.verify_signature(o_signature_public_key.value()));
if (validate_options.validate_signature) {
TRY_STATUS(block.verify_signature(o_signature_public_key.value()));
}
// 6. Applies the changes to the state.
// - If `validate_state_hash` is true, the state hash is validated.
// - Otherwise, the state hash is set to the hash of the block.
has_set_value_ = false;
has_shared_key_change_ = false;
has_group_state_change_ = false;
for (auto &change : block.changes_) {
TRY_STATUS(apply_change(change, o_signature_public_key.value(), validate_state_hash));
TRY_STATUS(apply_change(change, o_signature_public_key.value(), validate_options));
}
if (!validate_state_hash) {
if (!validate_options.validate_state_hash) {
TRY_STATUS(set_value_fast(block.state_proof_.kv_hash));
}
@ -466,14 +537,22 @@ td::Result<State> State::create_from_block(const Block &block, td::optional<td::
group_state = std::make_shared<GroupState>(GroupState{{}, GroupParticipantFlags::AllPermissions});
}
bool has_set_value = false;
bool has_group_state_change = false;
bool has_shared_key_change = false;
for (const auto &change_v : block.changes_) {
std::visit(td::overloaded([](const ChangeNoop &change) {}, [](const ChangeSetValue &change) {},
[&](const ChangeSetGroupState &change) {
group_state = change.group_state;
shared_key = GroupSharedKey::empty_shared_key();
},
[&](const ChangeSetSharedKey &change) { shared_key = change.shared_key; }),
change_v.value);
std::visit(
td::overloaded([](const ChangeNoop &change) {}, [&](const ChangeSetValue &change) { has_set_value = true; },
[&](const ChangeSetGroupState &change) {
group_state = change.group_state;
shared_key = GroupSharedKey::empty_shared_key();
has_group_state_change = true;
},
[&](const ChangeSetSharedKey &change) {
shared_key = change.shared_key;
has_shared_key_change = true;
}),
change_v.value);
}
if (block.state_proof_.o_group_state) {
@ -488,35 +567,57 @@ td::Result<State> State::create_from_block(const Block &block, td::optional<td::
if (!shared_key) {
return Error(E::InvalidBlock_InvalidStateProof_Secret, "no shared key");
}
return State(key_value_state, group_state, shared_key);
auto state = State(key_value_state, group_state, shared_key);
state.has_set_value_ = has_set_value;
state.has_group_state_change_ = has_group_state_change;
state.has_shared_key_change_ = has_shared_key_change;
TRY_STATUS(state.validate_state(block.state_proof_));
return state;
}
td::Result<Block> Blockchain::build_block(std::vector<Change> changes, const PrivateKey &private_key) const {
//TODO(now): check if we are allowed to sign this block
auto public_key = private_key.to_public_key();
auto state = state_;
if (last_block_.height_ == std::numeric_limits<td::int32>::max()) {
return td::Status::Error("Blockchain::build_block: last block height is too high");
}
td::int32 height = last_block_.height_ + 1;
if (height == 0) {
state.group_state_ = std::make_shared<GroupState>(GroupState{{}, GroupParticipantFlags::AllPermissions});
}
ValidateOptions validate_options;
validate_options.validate_state_hash = true;
validate_options.validate_signature = false;
validate_options.permissions = GroupParticipantFlags::AllPermissions;
for (const auto &change : changes) {
TRY_STATUS(state.apply_change(change, public_key, true));
TRY_STATUS(state.apply_change(change, public_key, validate_options));
}
StateProof state_proof;
state_proof.kv_hash = KeyValueHash{state.key_value_state_.get_hash()};
state_proof.o_group_state = state.group_state_;
state_proof.o_shared_key = state.shared_key_;
state.has_set_value_ = false;
state.has_group_state_change_ = false;
state.has_shared_key_change_ = false;
for (const auto &change_v : changes) {
std::visit(td::overloaded([](const ChangeNoop &change) {}, [](const ChangeSetValue &change) {},
std::visit(td::overloaded([](const ChangeNoop &change) {},
[&](const ChangeSetValue &change) { state.has_set_value_ = true; },
[&](const ChangeSetGroupState &change) {
state_proof.o_group_state = {};
state_proof.o_shared_key = {};
state.has_group_state_change_ = true;
},
[&](const ChangeSetSharedKey &change) { state_proof.o_shared_key = {}; }),
[&](const ChangeSetSharedKey &change) {
state_proof.o_shared_key = {};
state.has_shared_key_change_ = true;
}),
change_v.value);
}
TRY_STATUS(state.validate_state(state_proof));
Block block;
block.height_ = height;
@ -528,13 +629,13 @@ td::Result<Block> Blockchain::build_block(std::vector<Change> changes, const Pri
return block;
}
td::Status Blockchain::try_apply_block(Block block, bool validate_state_hash) {
td::Status Blockchain::try_apply_block(Block block, ValidateOptions validate_options) {
// To apply the first block an ephemeral -1 block is used
// - It has hash UInt256(0)
// - It has height -1
// - It has only one participant - Participant(user_id = 0, public_key = signer_public_key, permissions = all)
if (block.height_ != get_height() + 1) {
if (block.height_ != get_height() + 1 || get_height() == std::numeric_limits<td::int32>::max()) {
return Error(E::InvalidBlock_HeightMismatch,
PSLICE() << "new_block.height=" << block.height_ << " != 1 + last_block.height=" << get_height());
}
@ -545,10 +646,7 @@ td::Status Blockchain::try_apply_block(Block block, bool validate_state_hash) {
// TODO: validate total size of block
auto state = state_;
// TODO: use hint (state from build_block)
TRY_STATUS(state.apply(block, validate_state_hash));
TRY_STATUS(state.validate_state(block.state_proof_));
TRY_STATUS(state.apply(block, validate_options));
// NO errors after this point
state_ = std::move(state);
@ -567,14 +665,22 @@ td::int64 Blockchain::get_height() const {
return last_block_.height_;
}
td::UInt256 as_key(td::Slice key) {
CHECK(key.size() == 32);
td::Result<td::UInt256> as_key(td::Slice key) {
if (key.size() != 32) {
return td::Status::Error("Invalid key size");
}
td::UInt256 key_int256;
key_int256.as_mutable_slice().copy_from(key);
if (key_int256.is_zero()) {
return td::Status::Error("Invalid zero key");
}
return key_int256;
}
td::Result<Blockchain> Blockchain::create_from_block(Block block, td::optional<td::Slice> o_snapshot) {
if (block.height_ < 0) {
return Error(E::InvalidBlock, "negative height");
}
Blockchain res;
res.last_block_hash_ = block.calc_hash();
TRY_RESULT_ASSIGN(res.state_, State::create_from_block(block, std::move(o_snapshot)));
@ -582,11 +688,51 @@ td::Result<Blockchain> Blockchain::create_from_block(Block block, td::optional<t
return res;
}
namespace {
bool is_good_magic(td::int32 magic) {
return magic == td::e2e_api::e2e_chain_block::ID || magic == td::e2e_api::e2e_chain_groupBroadcastNonceCommit::ID ||
magic == td::e2e_api::e2e_chain_groupBroadcastNonceReveal::ID;
};
} // namespace
bool Blockchain::is_from_server(td::Slice block) {
if (block.size() < 4) {
return false;
}
td::int32 server_magic = td::as<td::int32>(block.data());
return is_good_magic(server_magic - 1) && !is_good_magic(server_magic);
}
td::Result<std::string> Blockchain::from_any_to_local(std::string block) {
if (is_from_server(block)) {
return from_server_to_local(std::move(block));
}
return block;
}
td::Result<std::string> Blockchain::from_server_to_local(std::string block) {
if (block.size() < 4) {
return td::Status::Error("block is too short");
}
td::int32 server_magic = td::as<td::int32>(block.data());
if (is_good_magic(server_magic)) {
return td::Status::Error("Trying to apply local block, not from server");
}
td::int32 real_magic = server_magic - 1;
td::as<td::int32>(block.data()) = real_magic;
return block;
}
td::Result<std::string> Blockchain::from_local_to_server(std::string block) {
if (block.size() < 4) {
return td::Status::Error("block is too short");
}
td::int32 magic = td::as<td::int32>(block.data());
td::as<td::int32>(block.data()) = magic + 1;
return block;
}
td::Result<ClientBlockchain> ClientBlockchain::create_from_block(td::Slice block_slice, const PublicKey &public_key) {
TRY_RESULT(block, Block::from_tl_serialized(block_slice));
TRY_RESULT(blockchain, Blockchain::create_from_block(std::move(block)));
// TODO: check public key is in blockchain
ClientBlockchain res;
res.blockchain_ = std::move(blockchain);
return res;
@ -601,11 +747,15 @@ td::Result<ClientBlockchain> ClientBlockchain::create_empty() {
td::Result<std::vector<Change>> ClientBlockchain::try_apply_block(td::Slice block_slice) {
TRY_RESULT(block, Block::from_tl_serialized(block_slice));
TRY_STATUS(blockchain_.try_apply_block(block, false));
ValidateOptions validate_options;
validate_options.validate_signature = true;
validate_options.validate_state_hash = false;
TRY_STATUS(blockchain_.try_apply_block(block, validate_options));
for (auto &change : block.changes_) {
if (std::holds_alternative<ChangeSetValue>(change.value)) {
auto &change_value = std::get<ChangeSetValue>(change.value);
map_[as_key(change_value.key)] = Entry{block.height_, change_value.value};
auto key = as_key(change_value.key).move_as_ok(); // already verified in try_apply_block
map_[key] = Entry{block.height_, change_value.value};
}
}
@ -626,17 +776,16 @@ td::Status ClientBlockchain::add_proof(td::Slice proof) {
td::Result<std::string> ClientBlockchain::build_block(const std::vector<Change> &changes,
const PrivateKey &private_key) const {
TRY_RESULT(block, blockchain_.build_block(changes, private_key));
//return serialize(*block.to_tl());
return block.to_tl_serialized();
;
}
td::Result<std::string> ClientBlockchain::get_value(td::Slice key) const {
auto it = map_.find(as_key(key));
td::Result<std::string> ClientBlockchain::get_value(td::Slice raw_key) const {
TRY_RESULT(key, as_key(raw_key));
auto it = map_.find(key);
if (it != map_.end()) {
return it->second.value;
}
return blockchain_.state_.key_value_state_.get_value(key);
return blockchain_.state_.key_value_state_.get_value(raw_key);
}
} // namespace tde2e_core

View File

@ -43,6 +43,7 @@ struct GroupParticipant {
td::int64 user_id{0};
td::int32 flags{0};
PublicKey public_key{};
td::int32 version{0};
bool add_users() const {
return (flags & GroupParticipantFlags::AddUsers) != 0;
}
@ -50,7 +51,8 @@ struct GroupParticipant {
return (flags & GroupParticipantFlags::RemoveUsers) != 0;
}
bool operator==(const GroupParticipant &other) const {
return user_id == other.user_id && flags == other.flags && public_key == other.public_key;
return user_id == other.user_id && flags == other.flags && public_key == other.public_key &&
version == other.version;
}
bool operator!=(const GroupParticipant &other) const {
return !(other == *this);
@ -61,7 +63,8 @@ struct GroupParticipant {
};
inline td::StringBuilder &operator<<(td::StringBuilder &sb, const GroupParticipant &part) {
return sb << "(uid=" << part.user_id << ", flags=" << part.flags << ", pk=" << part.public_key << ")";
return sb << "(uid=" << part.user_id << ", flags=" << part.flags << ", pk=" << part.public_key
<< ", version=" << part.version << ")";
}
struct GroupState;
@ -94,9 +97,10 @@ struct GroupState {
bool empty() const {
return participants.empty();
}
td::int32 version() const;
td::Result<GroupParticipant> get_participant(td::int64 user_id) const;
td::Result<GroupParticipant> get_participant(const PublicKey &public_key) const;
Permissions get_permissions(const PublicKey &public_key) const;
Permissions get_permissions(const PublicKey &public_key, td::int32 limit_permissions) const;
static GroupStateRef from_tl(const td::e2e_api::e2e_chain_groupState &state);
e2e::object_ptr<e2e::e2e_chain_groupState> to_tl() const;
static GroupStateRef empty_state();
@ -119,6 +123,9 @@ struct GroupSharedKey {
static GroupSharedKeyRef from_tl(const td::e2e_api::e2e_chain_sharedKey &shared_key);
e2e::object_ptr<e2e::e2e_chain_sharedKey> to_tl() const;
static GroupSharedKeyRef empty_shared_key();
bool empty() const {
return *this == *empty_shared_key();
}
bool operator==(const GroupSharedKey &other) const {
return ek == other.ek && encrypted_shared_key == other.encrypted_shared_key && dest_user_id == other.dest_user_id &&
dest_header == other.dest_header;
@ -201,13 +208,20 @@ struct StateProof {
static StateProof from_tl(const td::e2e_api::e2e_chain_stateProof &proof);
e2e::object_ptr<e2e::e2e_chain_stateProof> to_tl() const;
};
td::StringBuilder &operator<<(td::StringBuilder &sb, const StateProof &state);
struct ValidateOptions {
bool validate_state_hash{true};
bool validate_signature{true};
td::int32 permissions{GroupParticipantFlags::AllPermissions};
};
struct Block;
struct State {
KeyValueState key_value_state_;
GroupStateRef group_state_;
GroupSharedKeyRef shared_key_;
bool has_set_value_{};
bool has_shared_key_change_{};
bool has_group_state_change_{};
@ -223,18 +237,19 @@ struct State {
static State create_empty();
static td::Result<State> create_from_block(const Block &block, td::optional<td::Slice> o_snapshot = {});
// TODO snapshot..
// TODO apply
td::Status set_value(td::Slice key, td::Slice value, const Permissions &permissions);
td::Status set_group_state(GroupStateRef group_state, const Permissions &permissions);
td::Status clear_shared_key(const Permissions &permissions);
td::Status set_shared_key(GroupSharedKeyRef shared_key, const Permissions &permissions);
td::Status set_value_fast(KeyValueHash key_value_hash);
td::Status apply_change(const Change &change_outer, const PublicKey &public_key, bool full_apply);
td::Status set_value_fast(const KeyValueHash &key_value_hash);
td::Status apply_change(const Change &change_outer, const PublicKey &public_key, const ValidateOptions &options);
td::Status apply(Block &block, bool validate_state_hash = true);
td::Status apply(Block &block, ValidateOptions validate_options = {});
td::Status validate_state(const StateProof &state_proof) const;
static td::Status validate_group_state(const GroupStateRef &group_state);
static td::Status validate_shared_key(const GroupSharedKeyRef &shared_key, const GroupStateRef &group_state);
};
struct Block {
@ -270,8 +285,13 @@ struct Blockchain {
}
static td::Result<Blockchain> create_from_block(Block block, td::optional<td::Slice> o_snapshot = {});
static bool is_from_server(td::Slice block);
static td::Result<std::string> from_any_to_local(std::string block);
static td::Result<std::string> from_server_to_local(std::string block);
static td::Result<std::string> from_local_to_server(std::string block);
td::Result<Block> build_block(std::vector<Change> changes, const PrivateKey &private_key) const;
td::Status try_apply_block(Block block, bool validate_state_hash = true);
td::Status try_apply_block(Block block, ValidateOptions validate_options);
Block set_value(td::Slice key, td::Slice value, const PrivateKey &private_key) const;
td::int64 get_height() const;

View File

@ -2,7 +2,7 @@
#TODO
- version of supported encryption protocol in each participant (should use the lowest one)
- broadcast packets should be applied only after corresponding key is applied
- store hash of block in broadcast
## Overview
@ -45,7 +45,7 @@ The blockchain supports four types of changes:
2. **ChangeSetGroupState**: Updates the group of participants and their permissions
```
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true = e2e.chain.GroupParticipant;
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true version:int = e2e.chain.GroupParticipant;
e2e.chain.groupState participants:vector<e2e.chain.GroupParticipant> = e2e.chain.GroupState;
e2e.chain.changeSetGroupState group_state:e2e.chain.GroupState = e2e.chain.Change;
```
@ -68,7 +68,7 @@ Participants in the blockchain have specific permissions:
- **RemoveUsers**: Can remove existing participants from the blockchain
```
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true = e2e.chain.GroupParticipant;
e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true version:int = e2e.chain.GroupParticipant;
```
### Implementation Details
@ -185,5 +185,51 @@ The client library does not store the entire key-value state. To create a block,
1. It should be impossible to create a new key if the user is not in the group, even if the key is automatically removed after a state change.
2. It should be impossible to create a key if a participant has the Add or Remove permissions.
3. Statement (1) implies that it is impossible to remove yourself from the group.
# End of Selection
```
TODO:
+ handle short keys in SetValue
+ dest_user_id and dest_header must be the same size
+ check proto version in call encryption
+ Do I need some permissions to raise others flags -- NO, just Add AND Remove
+ validate group state from received block
+ validate keys
+ verify that generated block will indeed apply -- It is checked but, not explicitly
+ verify creator of network packet (pass user_id into decrypt method. Or even user_id and public key?)
+ sort nonces
+ what if we change nothing in block
+ fail if height overflows
+ CHECK(blockhain.get_height() > height_);
+ turn off the log by default
+ return specific error in get_status
+ optimize hmac
+ remove user_id from packet?
+ encrypt key (simple version)
+ channel_id in encryption
+ user_id instead of public_key in GroupBroadcast ?
+ received_messages_ in CallVerificationChain are not needed for clients
+ turn off signature verification on server
+ fix up magic of block on server
+ remove user_id from payload (we already got it externally)
- ! VERIFY block signature during creation blockchain from block
it is impossible to do currently using just current block, because we don't know who signe the block
later:
-+ fix tl constructor?
- add magic in container's id
- ttl of chain on server
-------
# Security guidance
There are several we should be really careful about.
1. Client must apply only blocks received from server. It is especially important for blocks created by the client itself. Sever does extra work to ensure correctness of blocks and that the will be no forks. Forks per se are not a security problem, but they would lead to a broken call
2. Blocks should be sent to the server till it some answer is received. Either it is success, or error. In case of error INVALID_BLOCK__HASH_MISMATCH, new block could be created, but one should be careful about how group state has been changed.
3. Broadcast blocks should also be sent till accepted or declined. Probably it should be allowed to skip older packets.

View File

@ -24,6 +24,7 @@
#include "td/utils/tl_parsers.h"
#include "td/utils/tl_storers.h"
#include <algorithm>
#include <limits>
#include <memory>
#include <utility>
@ -34,24 +35,38 @@ CallVerificationChain::State CallVerificationChain::get_state() const {
return state_;
}
template <class F>
struct LambdaStorer {
const F &store_;
};
template <class F, class StorerT>
void store(const LambdaStorer<F> &lambda_storer, StorerT &storer) {
lambda_storer.store_(storer);
}
template <class F>
std::string lambda_serialize(F &&f) {
return td::serialize(LambdaStorer<F>{std::forward<F>(f)});
}
void CallVerificationChain::on_new_main_block(const Blockchain &blockhain) {
state_ = Commit;
CHECK(blockhain.get_height() >= height_);
CHECK(blockhain.get_height() > height_);
height_ = td::narrow_cast<td::int32>(blockhain.get_height());
last_block_hash_ = blockhain.last_block_hash_;
verification_state_ = {};
verification_state_.height = height_;
verification_words_ = CallVerificationWords{blockhain.last_block_.height_,
Mnemonic::generate_verification_words(last_block_hash_.as_slice())};
auto group_state = *blockhain.state_.group_state_;
verification_words_ =
CallVerificationWords{height_, Mnemonic::generate_verification_words(last_block_hash_.as_slice())};
auto &group_state = *blockhain.state_.group_state_;
committed_ = {};
revealed_ = {};
received_messages_ = {};
participant_keys_ = {};
for (auto &participant : group_state.participants) {
participant_keys_.emplace(participant.public_key, participant.user_id);
participant_keys_.emplace(participant.user_id, participant.public_key);
}
CHECK(participant_keys_.size() == group_state.participants.size());
@ -69,20 +84,24 @@ td::Status CallVerificationChain::try_apply_block(td::Slice message) {
auto kv_broadcast = e2e::e2e_chain_GroupBroadcast::fetch(parser);
parser.fetch_end();
TRY_STATUS(parser.get_status());
td::Status status;
td::int32 broadcast_height{-1};
downcast_call(*kv_broadcast, td::overloaded([&](auto &broadcast) { broadcast_height = broadcast.chain_height_; }));
td::int32 chain_height{-1};
downcast_call(*kv_broadcast, td::overloaded([&](auto &broadcast) { chain_height = broadcast.chain_height_; }));
if (broadcast_height < height_) {
if (chain_height < height_) {
LOG(INFO) << "skip old broadcast " << to_short_string(kv_broadcast);
// broadcast is too old
return td::Status::OK();
}
if (broadcast_height > height_) {
if (chain_height > height_) {
if (!delay_allowed_) {
return Error(E::InvalidBroadcast_InFuture, PSLICE()
<< "broadcast_height=" << chain_height << " height=" << height_);
}
LOG(INFO) << "delay broadcast " << to_short_string(kv_broadcast);
delayed_broadcasts_[broadcast_height].emplace_back(message.str(), std::move(kv_broadcast));
delayed_broadcasts_[chain_height].emplace_back(message.str(), std::move(kv_broadcast));
return td::Status::OK();
}
@ -90,18 +109,17 @@ td::Status CallVerificationChain::try_apply_block(td::Slice message) {
}
std::string CallVerificationChain::to_short_string(e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> &broadcast) {
td::StringBuilder sb;
downcast_call(*broadcast, td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { sb << "CommitBroadcast"; },
[&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { sb << "RevealBroadcast"; }));
downcast_call(*broadcast,
td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { sb << "CommitBroadcast"; },
[&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { sb << "RevealBroadcast"; }));
downcast_call(*broadcast, [&](auto &v) {
sb << "{height=" << v.chain_height_;
auto public_key = PublicKey::from_u256(v.public_key_);
auto it = participant_keys_.find(public_key);
sb << "{height=" << v.chain_height_ << " user_id=" << v.user_id_;
auto it = participant_keys_.find(v.user_id_);
if (it != participant_keys_.end()) {
sb << " user_id=" << it->second;
sb << " pk=" << it->second;
} else {
sb << " user_id=?";
sb << " pk=?";
}
sb << " " << public_key;
sb << "}";
});
return sb.as_cslice().str();
@ -110,18 +128,22 @@ std::string CallVerificationChain::to_short_string(e2e::object_ptr<e2e::e2e_chai
td::Status CallVerificationChain::process_broadcast(std::string message,
e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast) {
td::Status status;
downcast_call(
*broadcast,
td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { status = process_broadcast(commit); },
[&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { status = process_broadcast(reveal); }));
td::UInt256 got_chain_hash{};
downcast_call(*broadcast, td::overloaded([&](auto &broadcast) { got_chain_hash = broadcast.chain_hash_; }));
if (got_chain_hash != last_block_hash_) {
status = Error(E::InvalidBroadcast_InvalidBlockHash);
}
if (status.is_ok()) {
received_messages_.push_back(std::move(message));
downcast_call(
*broadcast,
td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { status = process_broadcast(commit); },
[&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { status = process_broadcast(reveal); }));
}
if (status.is_error()) {
LOG(ERROR) << "Failed broadcast\n" << to_short_string(broadcast) << "\n\t" << status;
} else {
LOG(INFO) << "Applied broadcast\n\t" << to_short_string(broadcast) << "\n\t" << *this;
LOG(DEBUG) << "Applied broadcast\n\t" << to_short_string(broadcast) << "\n\t" << *this;
}
return status;
}
@ -132,28 +154,26 @@ CallVerificationState CallVerificationChain::get_verification_state() const {
CallVerificationWords CallVerificationChain::get_verification_words() const {
return verification_words_;
}
td::Span<std::string> CallVerificationChain::received_messages() const {
return received_messages_;
}
td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceCommit &nonce_commit) {
if (nonce_commit.chain_height_ != height_) {
return td::Status::Error(PSLICE() << "Invalid height expected=" << height_
<< " received=" << nonce_commit.chain_height_);
}
CHECK(nonce_commit.chain_height_ == height_);
if (state_ != Commit) {
return td::Status::Error("We are not in commit state");
return Error(E::InvalidBroadcast_NotInCommit);
}
auto public_key = PublicKey::from_u256(nonce_commit.public_key_);
if (participant_keys_.count(public_key) == 0) {
return td::Status::Error("NonceCommit: unknown public key");
auto user_id = nonce_commit.user_id_;
auto it = participant_keys_.find(user_id);
if (it == participant_keys_.end()) {
return Error(E::InvalidBroadcast_UnknownUserId);
}
TRY_STATUS(verify_signature(public_key, nonce_commit));
if (committed_.count(public_key) != 0) {
return td::Status::Error("NonceCommit: duplicate commit");
auto public_key = it->second;
if (!may_skip_signatures_validation_) {
TRY_STATUS(verify_signature(public_key, nonce_commit));
}
committed_[public_key] = nonce_commit.nonce_hash_.as_slice().str();
if (committed_.count(user_id) != 0) {
return Error(E::InvalidBroadcast_AlreadyApplied);
}
committed_[user_id] = nonce_commit.nonce_hash_.as_slice().str();
if (committed_.size() == participant_keys_.size()) {
state_ = Reveal;
@ -162,162 +182,237 @@ td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcas
return td::Status::OK();
}
td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceReveal &nonce_reveal) {
if (nonce_reveal.chain_height_ != height_) {
return td::Status::Error("NonceReveal: Invalid height");
}
CHECK(nonce_reveal.chain_height_ == height_);
if (state_ != Reveal) {
return td::Status::Error("We are not in reveal state");
return Error(E::InvalidBroadcast_NotInReveal);
}
auto public_key = PublicKey::from_u256(nonce_reveal.public_key_);
if (participant_keys_.count(public_key) == 0) {
return td::Status::Error("NonceReveal: unknown public key");
auto user_id = nonce_reveal.user_id_;
auto user_id_it = participant_keys_.find(user_id);
if (user_id_it == participant_keys_.end()) {
return Error(E::InvalidBroadcast_UnknownUserId);
}
TRY_STATUS(verify_signature(public_key, nonce_reveal));
if (revealed_.count(public_key) != 0) {
return td::Status::Error("NonceReveal: duplicate reveal");
auto public_key = user_id_it->second;
if (!may_skip_signatures_validation_) {
TRY_STATUS(verify_signature(public_key, nonce_reveal));
}
auto it = committed_.find(public_key);
if (revealed_.count(user_id) != 0) {
return Error(E::InvalidBroadcast_AlreadyApplied);
}
auto it = committed_.find(user_id);
CHECK(it != committed_.end());
auto expected_nonce_hash = it->second;
auto received_nonce_hash = td::sha256(nonce_reveal.nonce_.as_slice());
if (expected_nonce_hash != received_nonce_hash) {
return td::Status::Error("NonceReveal: hash(nonce) != nonce_hash");
return Error(E::InvalidBroadcast_InvalidReveal);
}
revealed_[public_key] = nonce_reveal.nonce_.as_slice().str();
revealed_[user_id] = nonce_reveal.nonce_.as_slice().str();
CHECK(!verification_state_.emoji_hash);
if (revealed_.size() == participant_keys_.size()) {
auto nonces = td::transform(revealed_, [](auto &p) { return p.second; });
std::sort(nonces.begin(), nonces.end());
std::string full_nonce;
for (auto &[key, nonce] : revealed_) {
for (auto &nonce : nonces) {
full_nonce += nonce;
}
verification_state_.emoji_hash =
MessageEncryption::combine_secrets(last_block_hash_.as_slice(), full_nonce).as_slice().str();
MessageEncryption::hmac_sha512(full_nonce, last_block_hash_.as_slice()).as_slice().str();
state_ = End;
}
return td::Status::OK();
}
CallEncryption::CallEncryption(PrivateKey private_key) : private_key_(std::move(private_key)) {
CallEncryption::CallEncryption(td::int64 user_id, PrivateKey private_key)
: user_id_(user_id), private_key_(std::move(private_key)) {
}
td::Status CallEncryption::add_shared_key(td::int32 epoch, td::SecureString key, GroupStateRef group_state) {
forget_old_epochs();
CHECK(!o_last_epoch_ || *o_last_epoch_ + 1 == epoch);
if (o_last_epoch_) {
epochs_to_forget_.emplace(td::Timestamp::in(10), *o_last_epoch_);
}
o_last_epoch_ = epoch;
sync();
TRY_RESULT(self, group_state->get_participant(private_key_.to_public_key()));
if (self.user_id != user_id_) {
// should not happen
return td::Status::Error("Wrong user id in state");
}
auto added =
encryptor_by_epoch_
.emplace(epoch, EpochEncryptor(epoch, self.user_id, std::move(key), std::move(group_state), private_key_))
.second;
LOG(INFO) << "Add key from epoch: " << epoch;
auto added = epochs_.emplace(epoch, EpochInfo(epoch, self.user_id, std::move(key), std::move(group_state))).second;
CHECK(added);
return td::Status::OK();
}
td::Result<std::string> CallEncryption::decrypt(td::Slice encrypted_data) {
forget_old_epochs();
void CallEncryption::forget_shared_key(td::int32 epoch) {
sync();
epochs_to_forget_.emplace(td::Timestamp::in(FORGET_EPOCH_DELAY), epoch);
}
td::Result<std::string> CallEncryption::decrypt(td::int64 user_id, td::int32 channel_id, td::Slice encrypted_data) {
sync();
if (user_id == user_id_) {
return td::Status::Error("Packet is encrypted by us");
}
td::TlParser parser(encrypted_data);
auto epoch = parser.fetch_int();
TRY_STATUS(parser.get_status());
auto it = encryptor_by_epoch_.find(epoch);
if (it == encryptor_by_epoch_.end()) {
return Error(E::Decrypt_UnknownEpoch);
auto head = static_cast<td::uint32>(parser.fetch_int());
td::int32 epochs_n = head & 0xff;
auto version = (head >> 8) & 0xff;
auto reserved = head >> 16;
if (version != 0) {
return td::Status::Error("Unsupported protocol version");
}
return it->second.decrypt(encrypted_data);
}
td::Result<std::string> CallEncryption::encrypt(td::Slice decrypted_data) {
CHECK(o_last_epoch_);
auto it = encryptor_by_epoch_.find(*o_last_epoch_);
if (it == encryptor_by_epoch_.end()) {
return Error(E::Encrypt_UnknownEpoch);
}
return it->second.encrypt(decrypted_data);
}
CallEncryption::EpochEncryptor::EpochEncryptor(td::int32 epoch, td::int64 user_id, td::SecureString secret,
GroupStateRef group_state, PrivateKey private_key)
: epoch_(epoch)
, user_id_(user_id)
, secret_(std::move(secret))
, group_state_(std::move(group_state))
, private_key_(std::move(private_key)) {
}
td::Result<std::string> CallEncryption::EpochEncryptor::decrypt(td::Slice encrypted_data) {
td::int32 epoch{};
using td::parse;
{
td::TlParser parser(encrypted_data);
parse(epoch, parser);
TRY_STATUS(parser.get_status());
if (reserved != 0) {
return td::Status::Error("reserved part of head is not zero");
}
TRY_RESULT(payload, MessageEncryption::decrypt_data(encrypted_data.substr(4), secret_));
if (epochs_n > MAX_ACTIVE_EPOCHS) {
return td::Status::Error("Too many active epochs");
}
using td::parse;
std::vector<td::int32> epochs;
for (int i = 0; i < epochs_n; i++) {
epochs.push_back(parser.fetch_int());
}
auto unencrypted_header = encrypted_data.substr(0, encrypted_data.size() - parser.get_left_len());
std::vector<td::Slice> encrypted_headers;
for (int i = 0; i < epochs_n; i++) {
auto encrypted_header = parser.fetch_string_raw<td::Slice>(32);
encrypted_headers.emplace_back(encrypted_header);
}
auto encrypted_packet = parser.fetch_string_raw<td::Slice>(parser.get_left_len());
parser.fetch_end();
TRY_STATUS(parser.get_status());
for (td::int32 i = 0; i < epochs_n; i++) {
auto epoch = epochs[i];
auto encrypted_header = encrypted_headers[i];
if (auto it = epochs_.find(epoch); it != epochs_.end()) {
auto &epoch_info = it->second;
TRY_RESULT(one_time_secret,
MessageEncryption::decrypt_header(encrypted_header, encrypted_packet, epoch_info.secret_));
return decrypt_packet_with_secret(user_id, channel_id, unencrypted_header, encrypted_packet, one_time_secret,
epoch_info.group_state_);
}
}
return Error(E::Decrypt_UnknownEpoch);
}
td::Result<std::string> CallEncryption::encrypt(td::int32 channel_id, td::Slice decrypted_data) {
sync();
// use all active epochs
if (epochs_.empty()) {
return Error(E::Encrypt_UnknownEpoch);
}
auto epochs_n = td::narrow_cast<td::int32>(epochs_.size());
using td::store;
std::string header_a = lambda_serialize([&](auto &storer) {
store(epochs_n, storer);
for (auto &[epoch_i, epoch] : epochs_) {
store(epoch_i, storer);
}
});
td::SecureString one_time_secret(32, 0);
td::Random::secure_bytes(one_time_secret.as_mutable_slice());
TRY_RESULT(encrypted_packet, encrypt_packet_with_secret(channel_id, header_a, decrypted_data, one_time_secret));
std::vector<td::SecureString> encrypted_headers;
for (auto &[epoch_i, epoch] : epochs_) {
TRY_RESULT(encrypted_header, MessageEncryption::encrypt_header(one_time_secret, encrypted_packet, epoch.secret_));
encrypted_headers.emplace_back(std::move(encrypted_header));
}
std::string header_b = lambda_serialize([&](auto &storer) {
for (auto &encrypted_header : encrypted_headers) {
CHECK(encrypted_header.size() == 32);
storer.store_slice(encrypted_header);
}
});
//LOG(ERROR) << decrypted_data.size() << " -> " << header_a.size() << " + " << header_b.size() << " + " << encrypted_packet.size();
return header_a + header_b + encrypted_packet;
}
td::Result<std::string> CallEncryption::encrypt_packet_with_secret(td::int32 channel_id, td::Slice unencrypted_part,
td::Slice packet, td::Slice one_time_secret) {
TRY_STATUS(validate_channel_id(channel_id));
auto &seqno = seqno_[channel_id];
if (seqno == std::numeric_limits<td::uint32>::max()) {
return td::Status::Error("Seqno overflow");
}
seqno++;
td::UInt512 zero_signature{};
auto payload = lambda_serialize([&](auto &storer) {
using td::store;
store(static_cast<td::int32>(channel_id), storer);
store(seqno, storer);
storer.store_slice(packet);
storer.store_slice(zero_signature.as_mutable_slice());
});
CHECK(payload.size() >= 64);
auto signature_offset = payload.size() - 64;
auto encrypted_part = td::Slice(payload.data(), signature_offset);
auto to_sign = MessageEncryption::hmac_sha512(unencrypted_part, encrypted_part);
TRY_RESULT(signature, private_key_.sign(to_sign));
td::MutableSlice(payload).substr(signature_offset).copy_from(signature.to_slice());
// TODO: there is too much copies happening here. Almost all of them could be avoided
return MessageEncryption::encrypt_data(payload, one_time_secret).as_slice().str();
}
td::Result<std::string> CallEncryption::decrypt_packet_with_secret(
td::int64 expected_user_id, td::int32 expected_channel_id, td::Slice unencrypted_header, td::Slice encrypted_packet,
td::Slice one_time_secret, const GroupStateRef &group_state) {
TRY_RESULT(participant, group_state->get_participant(expected_user_id));
TRY_RESULT(payload_str, MessageEncryption::decrypt_data(encrypted_packet, one_time_secret));
auto payload = td::Slice(payload_str);
if (payload.size() < 64) {
return td::Status::Error("Not enough encryption data");
}
auto signature_offset = payload.size() - 64;
auto encrypted_part = payload.substr(0, signature_offset);
auto to_verify = MessageEncryption::hmac_sha512(unencrypted_header, encrypted_part);
TRY_RESULT(signature, Signature::from_slice(payload.substr(signature_offset)));
TRY_STATUS(participant.public_key.verify(to_verify, signature));
// we know that this is packet create by some participant
td::TlParser parser(payload);
td::int64 user_id;
td::int32 channel_id;
td::uint32 seqno{};
td::UInt512 signature{};
parse(user_id, parser);
td::UInt512 signature_to_skip{};
parse(channel_id, parser);
TRY_STATUS(validate_channel_id(channel_id));
parse(seqno, parser);
if (parser.get_left_len() < 64) {
return td::Status::Error("Message is too short");
}
// TODO: check replay
auto result = parser.template fetch_string_raw<std::string>(parser.get_left_len() - 64);
parse(signature, parser);
parse(signature_to_skip, parser);
parser.fetch_end();
TRY_STATUS(parser.get_status());
TRY_STATUS(check_not_seen(user_id, seqno));
// verify signature
TRY_RESULT(participant, group_state_->get_participant(user_id));
TRY_STATUS(
participant.public_key.verify(td::Slice(payload.data(), payload.size() - 64), Signature::from_u512(signature)));
mark_as_seen(user_id, seqno);
if (channel_id != expected_channel_id) {
// currently ignore expected_channel_id
// return td::Status::Error("Channel id mismatch");
}
TRY_STATUS(check_not_seen(participant.public_key, channel_id, seqno));
mark_as_seen(participant.public_key, channel_id, seqno);
return result;
}
td::Result<std::string> CallEncryption::EpochEncryptor::encrypt(td::Slice decrypted_data) {
if (seqno_ == std::numeric_limits<td::uint32>::max()) {
return td::Status::Error("Seqno overflow");
}
seqno_++;
auto store = [&](auto &storer) {
using td::store;
store(user_id_, storer);
store(seqno_, storer);
storer.store_slice(decrypted_data);
};
td::TlStorerCalcLength calc_length;
store(calc_length);
auto length = calc_length.get_length();
std::string payload(length + 64, '\0');
td::TlStorerUnsafe storer(td::MutableSlice(payload).ubegin());
store(storer);
TRY_RESULT(signature, private_key_.sign(td::Slice(payload.data(), length)));
td::store(signature.to_u512(), storer);
// TODO: there is too much copies happening here. Almost all of them could be avoided
auto encrypted = MessageEncryption::encrypt_data(payload, secret_);
std::string res(4 + encrypted.size(), '\0');
td::TlStorerUnsafe res_storer(td::MutableSlice(res).ubegin());
td::store(epoch_, res_storer);
res_storer.store_slice(encrypted);
// LOG(ERROR) << decrypted_data.size() << " +info-> " << length << " +signature-> " << payload.size()
// << " +padding&msg_id-> " << encrypted.size() << " +epoch-> " << res.size();
return res;
}
td::Status CallEncryption::EpochEncryptor::check_not_seen(td::int64 user_id, td::uint32 seqno) {
auto &s = seen_[user_id];
td::Status CallEncryption::check_not_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) {
auto &s = seen_[std::make_pair(public_key, channel_id)];
if (s.empty()) {
return td::Status::OK();
}
@ -330,29 +425,40 @@ td::Status CallEncryption::EpochEncryptor::check_not_seen(td::int64 user_id, td:
}
return td::Status::OK();
}
void CallEncryption::EpochEncryptor::mark_as_seen(td::int64 user_id, td::uint32 seqno) {
void CallEncryption::mark_as_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) {
auto value = seqno;
auto &s = seen_[user_id];
auto &s = seen_[std::make_pair(public_key, channel_id)];
CHECK(s.insert(value).second);
while (s.size() > 1024) {
while (s.size() > 1024 || (!s.empty() && *s.begin() + 1024 < seqno)) {
s.erase(s.begin());
}
}
void CallEncryption::forget_old_epochs() {
if (epochs_to_forget_.empty()) {
return;
}
void CallEncryption::sync() {
auto now = td::Timestamp::now();
while (!epochs_to_forget_.empty() && epochs_to_forget_.front().first.is_in_past(now)) {
encryptor_by_epoch_.erase(epochs_to_forget_.front().second);
while (!epochs_to_forget_.empty() &&
(epochs_to_forget_.front().first.is_in_past(now) || epochs_.size() > MAX_ACTIVE_EPOCHS)) {
auto epoch = epochs_to_forget_.front().second;
LOG(INFO) << "Forget key from epoch: " << epoch;
epochs_.erase(epoch);
epochs_to_forget_.pop();
}
}
CallVerification CallVerification::create(PrivateKey private_key, const Blockchain &blockchain) {
td::Status CallEncryption::validate_channel_id(td::int32 channel_id) {
if (channel_id < 0 || channel_id > 1023) {
return Error(E::InvalidCallChannelId);
}
return td::Status::OK();
}
CallVerification CallVerification::create(td::int64 user_id, PrivateKey private_key, const Blockchain &blockchain) {
CallVerification result;
result.user_id_ = user_id;
result.private_key_ = std::move(private_key);
result.on_new_main_block(blockchain);
result.chain_.allow_delay();
return result;
}
@ -362,13 +468,13 @@ void CallVerification::on_new_main_block(const Blockchain &blockchain) {
td::sha256(nonce.as_mutable_slice(), nonce_hash.as_mutable_slice());
auto height = td::narrow_cast<td::int32>(blockchain.get_height());
auto nonce_commit_tl =
e2e::e2e_chain_groupBroadcastNonceCommit({}, private_key_.to_public_key().to_u256(), height, nonce_hash);
auto last_block_hash = blockchain.last_block_hash_;
auto nonce_commit_tl = e2e::e2e_chain_groupBroadcastNonceCommit({}, user_id_, height, last_block_hash, nonce_hash);
nonce_commit_tl.signature_ = sign(private_key_, nonce_commit_tl).move_as_ok().to_u512();
auto nonce_commit = serialize_boxed(nonce_commit_tl);
height_ = height;
;
last_block_hash_ = blockchain.last_block_hash_;
nonce_ = nonce;
sent_commit_ = true;
sent_reveal_ = false;
@ -394,21 +500,23 @@ td::Status CallVerification::receive_inbound_message(td::Slice message) {
if (chain_.get_state() == CallVerificationChain::Reveal && !sent_reveal_) {
sent_reveal_ = true;
auto nonce_reveal_tl =
e2e::e2e_chain_groupBroadcastNonceReveal({}, private_key_.to_public_key().to_u256(), height_, nonce_);
auto nonce_reveal_tl = e2e::e2e_chain_groupBroadcastNonceReveal({}, user_id_, height_, last_block_hash_, nonce_);
nonce_reveal_tl.signature_ = sign(private_key_, nonce_reveal_tl).move_as_ok().to_u512();
auto nonce_reveal = serialize_boxed(nonce_reveal_tl);
pending_outbound_messages_.clear();
CHECK(pending_outbound_messages_.empty());
pending_outbound_messages_.push_back(nonce_reveal);
}
return td::Status::OK();
}
Call::Call(PrivateKey pk, ClientBlockchain blockchain)
: private_key_(std::move(pk)), blockchain_(std::move(blockchain)), call_encryption_(private_key_) {
Call::Call(td::int64 user_id, PrivateKey pk, ClientBlockchain blockchain)
: user_id_(user_id)
, private_key_(std::move(pk))
, blockchain_(std::move(blockchain))
, call_encryption_(user_id, private_key_) {
CHECK(private_key_);
LOG(INFO) << "Create call \n" << *this;
call_verification_ = CallVerification::create(private_key_, blockchain_.get_inner_chain());
call_verification_ = CallVerification::create(user_id_, private_key_, blockchain_.get_inner_chain());
}
td::Result<std::string> Call::create_zero_block(const PrivateKey &private_key, GroupStateRef group_state) {
@ -416,8 +524,9 @@ td::Result<std::string> Call::create_zero_block(const PrivateKey &private_key, G
TRY_RESULT(changes, make_changes_for_new_state(std::move(group_state)));
return blockchain.build_block(changes, private_key);
}
td::Result<std::string> Call::create_self_add_block(const PrivateKey &private_key, td::Slice previous_block,
td::Result<std::string> Call::create_self_add_block(const PrivateKey &private_key, td::Slice previous_block_server,
const GroupParticipant &self) {
TRY_RESULT(previous_block, Blockchain::from_server_to_local(previous_block_server.str()));
TRY_RESULT(blockchain, ClientBlockchain::create_from_block(previous_block, private_key.to_public_key()));
auto old_state = *blockchain.get_group_state();
td::remove_if(old_state.participants,
@ -428,14 +537,16 @@ td::Result<std::string> Call::create_self_add_block(const PrivateKey &private_ke
return blockchain.build_block(changes, private_key);
}
td::Result<Call> Call::create(PrivateKey private_key, td::Slice last_block) {
td::Result<Call> Call::create(td::int64 user_id, PrivateKey private_key, td::Slice last_block_server) {
TRY_RESULT(last_block, Blockchain::from_server_to_local(last_block_server.str()));
TRY_RESULT(blockchain, ClientBlockchain::create_from_block(last_block, private_key.to_public_key()));
auto call = Call(std::move(private_key), std::move(blockchain));
auto call = Call(user_id, std::move(private_key), std::move(blockchain));
TRY_STATUS(call.update_group_shared_key());
return call;
}
td::Result<std::string> Call::build_change_state(GroupStateRef new_group_state) const {
TRY_STATUS(get_status());
TRY_RESULT(changes, make_changes_for_new_state(std::move(new_group_state)));
return blockchain_.build_block(changes, private_key_);
}
@ -456,7 +567,7 @@ td::Result<std::vector<Change>> Call::make_changes_for_new_state(GroupStateRef g
auto public_key = participant.public_key;
TRY_RESULT(shared_key, e_private_key.compute_shared_secret(public_key));
dst_user_id.push_back(participant.user_id);
auto header = MessageEncryption::encrypt_header(one_time_secret, encrypted_group_shared_key, shared_key);
TRY_RESULT(header, MessageEncryption::encrypt_header(one_time_secret, encrypted_group_shared_key, shared_key));
dst_header.push_back(header.as_slice().str());
}
auto change_set_shared_key = Change{ChangeSetSharedKey{std::make_shared<GroupSharedKey>(
@ -467,65 +578,77 @@ td::Result<std::vector<Change>> Call::make_changes_for_new_state(GroupStateRef g
return std::vector<Change>{std::move(change_set_group_state), std::move(change_set_shared_key)};
}
td::int32 Call::get_height() const {
td::Result<td::int32> Call::get_height() const {
TRY_STATUS(get_status());
return td::narrow_cast<td::int32>(blockchain_.get_height());
}
td::Result<GroupStateRef> Call::get_group_state() const {
TRY_STATUS(get_status());
return blockchain_.get_group_state();
}
td::Status Call::apply_block(td::Slice block) {
td::Status Call::apply_block(td::Slice server_block) {
TRY_STATUS(get_status());
TRY_RESULT(block, Blockchain::from_server_to_local(server_block.str()));
auto status = do_apply_block(block);
if (status.is_error()) {
LOG(ERROR) << "Failed to apply block: " << status << "\n" << Block::from_tl_serialized(block);
status_ = std::move(status);
} else {
LOG(INFO) << "Block has been applied\n" << *this;
}
return status;
return get_status();
}
td::Status Call::do_apply_block(td::Slice block) {
TRY_RESULT(changes, blockchain_.try_apply_block(block));
bool changed_shared_key = false;
for (auto &change : changes) {
if (std::holds_alternative<ChangeSetSharedKey>(change.value)) {
changed_shared_key = true;
}
if (std::holds_alternative<ChangeSetGroupState>(change.value)) {
changed_shared_key = true;
}
}
call_verification_.on_new_main_block(blockchain_.get_inner_chain());
if (changed_shared_key) {
TRY_STATUS(update_group_shared_key());
}
TRY_STATUS(update_group_shared_key());
return td::Status::OK();
}
td::Status Call::update_group_shared_key() {
td::Result<td::SecureString> Call::decrypt_shared_key() {
auto group_shared_key = blockchain_.get_group_shared_key();
auto group_state = blockchain_.get_group_state();
TRY_RESULT(participant, group_state->get_participant(private_key_.to_public_key()));
for (size_t i = 0; i < group_shared_key->dest_user_id.size(); i++) {
if (group_shared_key->dest_user_id[i] == participant.user_id) {
if (group_shared_key->dest_user_id[i] == user_id_) {
TRY_RESULT(shared_key, private_key_.compute_shared_secret(group_shared_key->ek));
TRY_RESULT(one_time_secret,
MessageEncryption::decrypt_header(group_shared_key->dest_header[i],
group_shared_key->encrypted_shared_key, shared_key));
TRY_RESULT(decrypted_group_shared_key,
TRY_RESULT(decrypted_shared_key,
MessageEncryption::decrypt_data(group_shared_key->encrypted_shared_key, one_time_secret));
group_shared_key_ = std::move(decrypted_group_shared_key);
return call_encryption_.add_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height()),
group_shared_key_.copy(), group_state);
if (decrypted_shared_key.size() != 32) {
return td::Status::Error("Invalid shared key (size != 32)");
}
return decrypted_shared_key;
}
}
group_shared_key_ = td::SecureString();
return td::Status::Error("Could not find user_id in group_shared_key");
}
td::Status Call::update_group_shared_key() {
// NB: we drop key immediately, we don't want old key to be active due to some errors later
group_shared_key_ = {};
call_encryption_.forget_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height() - 1));
auto group_state = blockchain_.get_group_state();
auto r_participant = group_state->get_participant(private_key_.to_public_key());
if (r_participant.is_error()) {
return Error(E::InvalidCallGroupState_NotParticipant);
}
auto participant = r_participant.move_as_ok();
if (participant.user_id != user_id_) {
return Error(E::InvalidCallGroupState_WrongUserId);
}
TRY_RESULT_ASSIGN(group_shared_key_, decrypt_shared_key());
return call_encryption_.add_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height()), group_shared_key_.copy(),
group_state);
}
td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerificationChain &chain) {
sb << "Verification {height=" << chain.height_ << " state=";
switch (chain.state_) {
@ -559,12 +682,15 @@ td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerification &ver
return sb << verification.chain_;
}
td::StringBuilder &operator<<(td::StringBuilder &sb, const Call &call) {
sb << "Call{" << call.get_height() << ":" << call.private_key_.to_public_key() << "}";
auto group_state = call.get_group_state().move_as_ok();
auto status = call.get_status();
sb << "Call{" << call.blockchain_.get_height() << ":" << call.private_key_.to_public_key() << "}";
if (status.is_error()) {
sb << "\nCALL_FAILED: " << call.status_;
}
auto group_state = call.blockchain_.get_group_state();
sb << "\n\tusers=" << td::transform(group_state->participants, [](auto &p) { return p.user_id; });
sb << "\n\tpkeys=" << td::transform(group_state->participants, [](auto &p) { return p.public_key; });
sb << "\n\t" << call.call_verification_;
return sb;
}
} // namespace tde2e_core

View File

@ -39,16 +39,21 @@ struct CallVerificationChain {
void on_new_main_block(const Blockchain &blockhain);
td::Status try_apply_block(td::Slice message);
std::string to_short_string(e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> &broadcast);
td::Status process_broadcast(std::string message, e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast);
CallVerificationState get_verification_state() const;
CallVerificationWords get_verification_words() const;
td::Span<std::string> received_messages() const;
void allow_delay() {
delay_allowed_ = true;
}
void skip_signatures_validation() {
may_skip_signatures_validation_ = true;
}
friend td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerificationChain &chain);
private:
td::Status process_broadcast(std::string message, e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast);
td::Status process_broadcast(e2e::e2e_chain_groupBroadcastNonceCommit &nonce_commit);
td::Status process_broadcast(e2e::e2e_chain_groupBroadcastNonceReveal &nonce_reveal);
@ -57,60 +62,62 @@ struct CallVerificationChain {
CallVerificationWords verification_words_;
td::int32 height_{-1};
td::UInt256 last_block_hash_{};
std::map<PublicKey, td::int64> participant_keys_;
std::map<PublicKey, std::string> committed_;
std::map<PublicKey, std::string> revealed_;
std::vector<std::string> received_messages_;
std::map<td::int64, PublicKey> participant_keys_;
std::map<td::int64, std::string> committed_;
std::map<td::int64, std::string> revealed_;
bool delay_allowed_{false};
bool may_skip_signatures_validation_{false};
std::map<td::int32, std::vector<std::pair<std::string, e2e::object_ptr<e2e::e2e_chain_GroupBroadcast>>>>
delayed_broadcasts_;
};
class CallEncryption {
public:
explicit CallEncryption(PrivateKey private_key);
explicit CallEncryption(td::int64 user_id, PrivateKey private_key);
td::Status add_shared_key(td::int32 epoch, td::SecureString key, GroupStateRef group_state);
void forget_shared_key(td::int32 epoch);
td::Result<std::string> decrypt(td::Slice encrypted_data);
td::Result<std::string> encrypt(td::Slice decrypted_data);
td::Result<std::string> decrypt(td::int64 expected_user_id, td::int32 expected_channel_id, td::Slice encrypted_data);
td::Result<std::string> encrypt(td::int32 channel_id, td::Slice decrypted_data);
private:
static constexpr double FORGET_EPOCH_DELAY = 10;
static constexpr int MAX_ACTIVE_EPOCHS = 15;
td::int64 user_id_{};
PrivateKey private_key_;
class EpochEncryptor {
public:
EpochEncryptor(td::int32 epoch, td::int64 user_id, td::SecureString secret, GroupStateRef group_state,
PrivateKey private_key);
struct EpochInfo {
EpochInfo(td::int32 epoch, td::int64 user_id, td::SecureString secret, GroupStateRef group_state)
: epoch_(epoch), user_id_(user_id), secret_(std::move(secret)), group_state_(std::move(group_state)) {
}
td::Result<std::string> decrypt(td::Slice encrypted_data);
td::Result<std::string> encrypt(td::Slice decrypted_data);
td::Status check_not_seen(td::int64 user_id, td::uint32 seqno);
void mark_as_seen(td::int64 user_id, td::uint32 seqno);
private:
td::int32 epoch_;
td::int64 user_id_;
td::uint32 seqno_{0};
td::int32 epoch_{};
td::int64 user_id_{};
td::SecureString secret_;
GroupStateRef group_state_;
PrivateKey private_key_;
std::unordered_map<td::int64, std::set<td::uint32>, td::Hash<td::int64>> seen_;
};
td::optional<td::int32> o_last_epoch_;
std::map<td::int32, EpochEncryptor> encryptor_by_epoch_;
std::map<td::int32, td::uint32> seqno_;
std::map<td::int32, EpochInfo> epochs_;
td::VectorQueue<std::pair<td::Timestamp, td::int32>> epochs_to_forget_;
std::map<std::pair<PublicKey, td::int32>, std::set<td::uint32>> seen_;
void forget_old_epochs();
void sync();
td::Result<std::string> encrypt_packet_with_secret(td::int32 channel_id, td::Slice header, td::Slice packet,
td::Slice one_time_secret);
td::Result<std::string> decrypt_packet_with_secret(td::int64 expected_user_id, td::int32 expected_channel_id,
td::Slice unencrypted_packet, td::Slice encrypted_packet,
td::Slice one_time_secret, const GroupStateRef &group_state);
td::Status check_not_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno);
void mark_as_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno);
static td::Status validate_channel_id(td::int32 channel_id);
};
class CallVerification {
public:
static CallVerification create(PrivateKey private_key, const Blockchain &blockchain);
static CallVerification create(td::int64 user_id, PrivateKey private_key, const Blockchain &blockchain);
void on_new_main_block(const Blockchain &blockhain);
CallVerificationState get_verification_state() const;
std::vector<std::string> pull_outbound_messages();
@ -120,68 +127,89 @@ class CallVerification {
friend td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerification &verification);
private:
td::int64 user_id_{};
PrivateKey private_key_;
CallVerificationChain chain_;
std::vector<tde2e_api::Bytes> pending_outbound_messages_;
bool sent_commit_{false};
bool sent_reveal_{false};
td::int32 height_{};
td::int32 height_{-1};
td::UInt256 last_block_hash_{};
td::UInt256 nonce_{};
};
struct Call {
Call(PrivateKey pk, ClientBlockchain blockchain);
static td::Result<std::string> create_zero_block(const PrivateKey &private_key, GroupStateRef group_state);
static td::Result<std::string> create_self_add_block(const PrivateKey &private_key, td::Slice previous_block,
const GroupParticipant &self);
static td::Result<Call> create(PrivateKey private_key, td::Slice last_block);
td::Result<std::string> build_change_state(GroupStateRef new_group_state) const;
static td::Result<Call> create(td::int64 user_id, PrivateKey private_key, td::Slice last_block);
static td::Result<std::vector<Change>> make_changes_for_new_state(GroupStateRef group_state);
td::int32 get_height() const;
td::Result<std::string> build_change_state(GroupStateRef new_group_state) const;
td::Result<td::int32> get_height() const;
td::Result<GroupStateRef> get_group_state() const;
// TODO: add self user_id
// TODO: verify that call contains us
// changes CallVerificationState
td::Status apply_block(td::Slice block);
td::Slice shared_key() const {
return group_shared_key_;
}
td::Result<std::string> decrypt(td::Slice encrypted_data) {
return call_encryption_.decrypt(encrypted_data);
}
td::Result<std::string> encrypt(td::Slice decrypted_data) {
return call_encryption_.encrypt(decrypted_data);
td::Status get_status() const {
if (status_.is_error()) {
return Error(E::CallFailed, PSLICE() << status_);
}
return td::Status::OK();
}
std::vector<std::string> pull_outbound_messages() {
td::Result<td::SecureString> shared_key() const {
TRY_STATUS(get_status());
return group_shared_key_.copy();
}
td::Result<std::string> decrypt(td::int64 user_id, td::int32 channel_id, td::Slice encrypted_data) {
TRY_STATUS(get_status());
return call_encryption_.decrypt(user_id, channel_id, encrypted_data);
}
td::Result<std::string> encrypt(td::int32 channel_id, td::Slice decrypted_data) {
TRY_STATUS(get_status());
return call_encryption_.encrypt(channel_id, decrypted_data);
}
td::Result<std::vector<std::string>> pull_outbound_messages() {
TRY_STATUS(get_status());
return call_verification_.pull_outbound_messages();
}
td::Result<CallVerificationState> get_verification_state() const {
TRY_STATUS(get_status());
return call_verification_.get_verification_state();
}
td::Result<CallVerificationWords> get_verification_words() const {
TRY_STATUS(get_status());
return call_verification_.get_verification_words();
}
td::Result<CallVerificationState> receive_inbound_message(td::Slice verification_message) {
TRY_STATUS(call_verification_.receive_inbound_message(verification_message));
TRY_STATUS(get_status());
// For now, don't fail the call in case of some errors
TRY_RESULT(local_verification_message, Blockchain::from_server_to_local(verification_message.str()));
TRY_STATUS(call_verification_.receive_inbound_message(local_verification_message));
return get_verification_state();
}
friend td::StringBuilder &operator<<(td::StringBuilder &builder, const Call &call);
private:
td::Status status_{td::Status::OK()};
td::int64 user_id_{0};
PrivateKey private_key_;
ClientBlockchain blockchain_;
td::SecureString group_shared_key_;
CallVerification call_verification_;
CallEncryption call_encryption_;
td::SecureString group_shared_key_;
Call(td::int64 user_id, PrivateKey pk, ClientBlockchain blockchain);
td::Status update_group_shared_key();
td::Status do_apply_block(td::Slice block);
td::Result<td::SecureString> decrypt_shared_key();
};
} // namespace tde2e_core

View File

@ -24,7 +24,7 @@ DecryptedKey::DecryptedKey(RawDecryptedKey key)
}
EncryptedKey DecryptedKey::encrypt(td::Slice local_password, td::Slice secret) const {
td::SecureString decrypted_secret = MessageEncryption::combine_secrets(secret, local_password);
td::SecureString decrypted_secret = MessageEncryption::hmac_sha512(secret, local_password);
td::SecureString encryption_secret =
MessageEncryption::kdf(as_slice(decrypted_secret), "tde2e local key", EncryptedKey::PBKDF_ITERATIONS);

View File

@ -19,7 +19,7 @@ td::Result<DecryptedKey> EncryptedKey::decrypt(td::Slice local_password, bool ch
return td::Status::Error("Failed to decrypt key: invalid secret size");
}
*/
auto decrypted_secret = MessageEncryption::combine_secrets(secret, local_password);
auto decrypted_secret = MessageEncryption::hmac_sha512(secret, local_password);
td::SecureString encryption_secret =
MessageEncryption::kdf(as_slice(decrypted_secret), "tde2e local key", EncryptedKey::PBKDF_ITERATIONS);

View File

@ -18,6 +18,7 @@ namespace tde2e_core {
struct DecryptedKey;
struct EncryptedKey {
static constexpr int PBKDF_ITERATIONS = 100000;
static constexpr int PBKDF_FAST_ITERATIONS = 1;
td::SecureString encrypted_data;
td::optional<PublicKey> o_public_key;
td::SecureString secret;

View File

@ -323,9 +323,9 @@ struct EncryptedStorage {
static td::Result<EncryptedStorage> create(td::Slice last_block, PrivateKey pk) {
auto public_key = pk.to_public_key();
auto secret_for_key = MessageEncryption::combine_secrets("EncryptedStorage::secret_for_key", pk.to_secure_string());
auto secret_for_key = MessageEncryption::hmac_sha512(pk.to_secure_string(), "EncryptedStorage::secret_for_key");
auto secret_for_value =
MessageEncryption::combine_secrets("EncryptedStorage::secret_for_value", pk.to_secure_string());
MessageEncryption::hmac_sha512(pk.to_secure_string(), "EncryptedStorage::secret_for_value");
ClientBlockchain blockchain;
if (last_block.empty()) {
TRY_RESULT_ASSIGN(blockchain, ClientBlockchain::create_empty());

View File

@ -31,38 +31,54 @@ We use several encryption primitives similar to MTProto 2.0. Here are the key fu
1) padding_size = ((16 + payload.size + 15) & -16) - payload.size
2) padding = random_bytes(padding_size) with padding[0] = padding_size
3) padded_data = padding || payload
4) data_hash = SHA256(padded_data)
5) (aes_key, aes_iv) = SHA512(data_hash || secret)[0:48]
6) encrypted = aes_cbc(aes_key, aes_iv, padded_data)
7) result = data_hash || encrypted
4) large_secret = KDF(secret, "tde2e_encrypt_data")
5) encrypt_secret = larges_secret[0:32]
6) hmac_secret = large_secret[32:64]
7) msg_id = HMAC-SHA512(hmac_secret, padded_data)[0:16]
8) (aes_key, aes_iv) = HMAC-SHA512(encrypt_secret, msg_id)[0:48]
9) encrypted = aes_cbc(aes_key, aes_iv, padded_data)
10) result = msg_id || encrypted
#### encrypt_header(header, encrypted_msg, secret) - encrypts 32-byte header
1) msg_hash = encrypted_msg[0:32] // First 32 bytes
2) (aes_key, aes_iv) = SHA512(msg_hash || secret)[0:48]
3) encrypted_header = aes_cbc(aes_key, aes_iv, header)
1) msg_id = encrypted_msg[0:16] // First 16 bytes
2) encrypt_secret = KDF(secret, "tde2e_encrypt_header")[0:32]
3) (aes_key, aes_iv) = HMAC-SHA512(encrypt_secret, msg_id)[0:48]
4) encrypted_header = aes_cbc(aes_key, aes_iv, header)
Decryption must verify `data_hash` before any further checks.
KDF = HMAC-SHA512, also
Decryption must verify that the message has a valid `msg_id` by computing HMAC over the decrypted data and comparing with the message's msg_id before accepting the payload.
### Packet encryption
This is how we encrypt actual packets:
#### encrypt_packet(payload, epoch_shared_key, user_id, seq_num, private_key) - encrypts a packet
#### encrypt_packet(payload, active_epochs, user_id, channel_id, seq_num, private_key) - encrypts a packet
1) packet_header = epoch (4 bytes)
2) packet_payload = user_id (8 bytes) || seq_num (4 bytes) || payload
3) signature = sign(packet_payload, private_key) // 64 bytes
4) signed_payload = packet_payload || signature
5) encrypted_payload = encrypt_data(signed_payload, epoch_shared_key)
6) result = packet_header || encrypted_payload
First, we generate header_a describing epochs (blockchain heights) used
1) epoch_id[i] = active_epochs[i].epoch (4 bytes)
2) header_a = active_epochs.size (4 bytes) || epoch_id[0] || epoch_id[1] || ...
Key properties:
- Each packet includes 4-byte epoch to identify encryption key
- Payload contains 8-byte user ID and 4-byte sequence number
- Sequence numbers track last 1024 per user to prevent replays
- Payload is signed with sender's private key (64-byte signature)
- Full payload is encrypted using encrypt_data() with epoch's shared key
Then, we encrypt payload with one_time_key. Signature in payload includes unencrypted header
1) one_time_key = random(32)
2) packet_payload = channel_id (4 bytes) || seq_num (4 bytes) || payload
3) to_sign = HMAC-SHA512(header_a, packet_payload)
4) signature = sign(to_sign, private_key) // 64 bytes
5) signed_payload = packet_payload || signature
6) encrypted_payload = encrypt_data(signed_payload, one_time_key)
Finally, encrypt one_time_key with shared secret from each active epoch
1) encrypted_key[i] = encrypt_header(one_time_key, encrypted_payload, active_epochs[i].shared_key) (32 bytes)
2) header_b = encrypted_key[0] || encrypted_key[1] || ...
result = header_a || header_b || encrypted_payload
- seqno is unique for each pair (public key; channel_id), so it is used as protection from replay attacks
- list of active epochs is also signed
During decryption, we must take public key from blockchain's state. We also must verify user_id and channel_id. Expected user_id and channel_id are known externally
### Encryption of shared key
@ -109,10 +125,10 @@ The emoji hash generation uses a two-phase commit-reveal protocol to prevent bru
- Concatenate all revealed nonces in order
- `emoji_hash = SHA512(blockchain_hash || concatenated_sorted_nonces)`
Tl schema used for such broadcast is the following
Tl schema used for such broadcast is the following
```
e2e.chain.groupBroadcastNonceCommit signature:int512 public_key:int256 chain_height:int32 nonce_hash:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupBroadcastNonceReveal signature:int512 public_key:int256 chain_height:int32 nonce:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupBroadcastNonceCommit signature:int512 public_key:int256 chain_height:int32 chain_hash:int256 nonce_hash:int256 = e2e.chain.GroupBroadcast;
e2e.chain.groupBroadcastNonceReveal signature:int512 public_key:int256 chain_height:int32 chain_hash:int256 nonce:int256 = e2e.chain.GroupBroadcast;
```
The signature is for the TL serialization of the same object with zeroed signature
The signature is for the TL serialization of the same object with zeroed signature

View File

@ -4,6 +4,7 @@
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "MessageEncryption.h"
#include "td/e2e/Keys.h"
#include "td/utils/Ed25519.h"
@ -107,7 +108,9 @@ td::Result<PrivateKey> PrivateKey::from_slice(const td::Slice &slice) {
return std::make_shared<PrivateKeyRaw>(PrivateKeyRaw{{std::move(public_key)}, std::move(private_key)});
}
td::Result<td::SecureString> PrivateKey::compute_shared_secret(const PublicKey &public_key) const {
return td::Ed25519::compute_shared_secret(public_key.raw().public_key, raw_->private_key);
TRY_RESULT(x25519_shared_secret, td::Ed25519::compute_shared_secret(public_key.raw().public_key, raw_->private_key));
return td::SecureString(
MessageEncryption::hmac_sha512("tde2e_shared_secret", x25519_shared_secret).as_slice().substr(0, 32));
}
td::Result<Signature> PrivateKey::sign(const td::Slice &data) const {
CHECK(raw_);

View File

@ -28,26 +28,22 @@ td::AesCbcState MessageEncryption::calc_aes_cbc_state_from_hash(td::Slice hash)
return td::AesCbcState{key, iv};
}
td::AesCbcState MessageEncryption::calc_aes_cbc_state_from_secret(td::Slice seed) {
td::SecureString hash(64);
sha512(seed, hash.as_mutable_slice());
return calc_aes_cbc_state_from_hash(hash.as_slice().substr(0, 48));
}
td::SecureString MessageEncryption::gen_random_prefix(td::int64 data_size, td::int64 min_padding) {
td::SecureString buff(td::narrow_cast<size_t>(((min_padding + 15 + data_size) & -16) - data_size), 0);
td::SecureString buff(td::narrow_cast<size_t>(((min_padding + 15 + data_size) & ~static_cast<td::int64>(15)) - data_size), '\0');
td::Random::secure_bytes(buff.as_mutable_slice());
buff.as_mutable_slice()[0] = td::narrow_cast<td::uint8>(buff.size());
buff.as_mutable_slice().ubegin()[0] = td::narrow_cast<td::uint8>(buff.size());
CHECK((buff.size() + data_size) % 16 == 0);
return buff;
}
td::SecureString MessageEncryption::combine_secrets(td::Slice a, td::Slice b) {
td::SecureString res(64, 0);
hmac_sha512(a, b, res.as_mutable_slice());
return res;
td::SecureString MessageEncryption::gen_deterministic_prefix(td::int64 data_size, td::int64 min_padding) {
td::SecureString buff(td::narrow_cast<size_t>(((min_padding + 15 + data_size) & ~static_cast<td::int64>(15)) - data_size), '\0');
buff.as_mutable_slice().ubegin()[0] = td::narrow_cast<td::uint8>(buff.size());
CHECK((buff.size() + data_size) % 16 == 0);
return buff;
}
td::SecureString MessageEncryption::kdf(td::Slice secret, td::Slice password, int iterations) {
td::SecureString new_secret(64);
pbkdf2_sha512(secret, password, iterations, new_secret.as_mutable_slice());
@ -56,17 +52,25 @@ td::SecureString MessageEncryption::kdf(td::Slice secret, td::Slice password, in
td::SecureString MessageEncryption::encrypt_data_with_prefix(td::Slice data, td::Slice secret) {
CHECK(data.size() % 16 == 0);
auto data_hash = sha256(data);
auto large_secret = kdf_expand(secret, "tde2e_encrypt_data");
auto encrypt_secret = large_secret.as_slice().substr(0, 32);
auto hmac_secret = large_secret.as_mutable_slice().substr(32, 32);
td::SecureString res_buf(data.size() + 32, 0);
auto large_msg_id = hmac_sha512(hmac_secret, data);
auto msg_id = large_msg_id.as_slice().substr(0, 16);
td::SecureString res_buf(data.size() + 16, '\0');
auto res = res_buf.as_mutable_slice();
res.copy_from(data_hash);
res.copy_from(msg_id);
auto cbc_state = calc_aes_cbc_state_from_hash(combine_secrets(data_hash, secret));
cbc_state.encrypt(data, res.substr(32));
auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encrypt_secret, msg_id));
cbc_state.encrypt(data, res.substr(16));
return res_buf;
}
td::SecureString MessageEncryption::kdf_expand(td::Slice random_secret, td::Slice info) {
return hmac_sha512(random_secret, info);
}
td::SecureString MessageEncryption::encrypt_data(td::Slice data, td::Slice secret) {
auto prefix = gen_random_prefix(data.size(), MIN_PADDING);
@ -77,22 +81,34 @@ td::SecureString MessageEncryption::encrypt_data(td::Slice data, td::Slice secre
}
td::Result<td::SecureString> MessageEncryption::decrypt_data(td::Slice encrypted_data, td::Slice secret) {
if (encrypted_data.size() < 33) {
return td::Status::Error("Failed to decrypt: data is too small");
if (encrypted_data.size() < 16) {
return td::Status::Error("Failed to decrypt: encrypted_data is less than 16 bytes");
}
if (encrypted_data.size() % 16 != 0) {
return td::Status::Error("Failed to decrypt: data size is not divisible by 16");
}
auto data_hash = encrypted_data.substr(0, 32);
encrypted_data = encrypted_data.substr(32);
auto cbc_state = calc_aes_cbc_state_from_hash(combine_secrets(data_hash, secret));
td::SecureString decrypted_data(encrypted_data.size(), 0);
auto large_secret = kdf_expand(secret, "tde2e_encrypt_data");
auto encrypt_secret = large_secret.as_slice().substr(0, 32);
auto hmac_secret = large_secret.as_mutable_slice().substr(32, 32);
auto msg_id = encrypted_data.substr(0, 16);
encrypted_data = encrypted_data.substr(16);
auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encrypt_secret, msg_id));
td::SecureString decrypted_data(encrypted_data.size(), '\0');
cbc_state.decrypt(encrypted_data, decrypted_data.as_mutable_slice());
auto got_large_msg_id = hmac_sha512(hmac_secret, decrypted_data);
auto got_msg_id = got_large_msg_id.as_slice().substr(0, 16);
// check hash
if (data_hash != td::sha256(decrypted_data)) {
return td::Status::Error("Failed to decrypt: hash mismatch");
int is_mac_bad = 0;
for (size_t i = 0; i < 16; i++) {
is_mac_bad |= got_msg_id[i] ^ msg_id[i];
}
if (is_mac_bad != 0) {
return td::Status::Error("Failed to decrypt: msg_id mismatch");
}
auto prefix_size = static_cast<td::uint8>(decrypted_data[0]);
@ -103,12 +119,25 @@ td::Result<td::SecureString> MessageEncryption::decrypt_data(td::Slice encrypted
return td::SecureString(decrypted_data.as_slice().substr(prefix_size));
}
td::SecureString MessageEncryption::encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message,
td::Slice secret) {
CHECK(encrypted_message.size() >= 32);
CHECK(decrypted_header.size() == 32);
auto data_hash = encrypted_message.substr(0, 32);
auto cbc_state = calc_aes_cbc_state_from_hash(combine_secrets(data_hash, secret));
td::SecureString MessageEncryption::hmac_sha512(td::Slice key, td::Slice message) {
td::SecureString res(64, 0);
td::hmac_sha512(key, message, res.as_mutable_slice());
return res;
}
td::Result<td::SecureString> MessageEncryption::encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message,
td::Slice secret) {
if (encrypted_message.size() < 16) {
return td::Status::Error("Failed to encrypt header: encrypted_message is too small");
}
if (decrypted_header.size() != 32) {
return td::Status::Error("Failed to encrypt header: header must be 32 bytes");
}
auto large_key = kdf_expand(secret, "tde2e_encrypt_header");
auto encryption_key = large_key.as_slice().substr(0, 32);
auto msg_id = encrypted_message.substr(0, 16);
auto cbc_state = calc_aes_cbc_state_from_hash(kdf_expand(encryption_key, msg_id));
td::SecureString encrypted_header(32, 0);
cbc_state.encrypt(decrypted_header, encrypted_header.as_mutable_slice());
@ -117,15 +146,18 @@ td::SecureString MessageEncryption::encrypt_header(td::Slice decrypted_header, t
td::Result<td::SecureString> MessageEncryption::decrypt_header(td::Slice encrypted_header, td::Slice encrypted_message,
td::Slice secret) {
if (encrypted_message.size() < 16) {
return td::Status::Error("Failed to decrypt: invalid message size");
}
if (encrypted_header.size() != 32) {
return td::Status::Error("Failed to decrypt: invalid header size");
}
if (encrypted_message.size() < 32) {
return td::Status::Error("Failed to decrypt: invalid message size");
}
auto data_hash = encrypted_message.substr(0, 32);
auto cbc_state = calc_aes_cbc_state_from_hash(combine_secrets(data_hash, secret));
auto large_key = kdf_expand(secret, "tde2e_encrypt_header");
auto encryption_key = large_key.as_slice().substr(0, 32);
auto msg_id = encrypted_message.substr(0, 16);
auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encryption_key, msg_id));
td::SecureString decrypted_header(32, 0);
cbc_state.decrypt(encrypted_header, decrypted_header.as_mutable_slice());

View File

@ -17,20 +17,23 @@ class MessageEncryption {
public:
static td::SecureString encrypt_data(td::Slice data, td::Slice secret);
static td::Result<td::SecureString> decrypt_data(td::Slice encrypted_data, td::Slice secret);
static td::SecureString combine_secrets(td::Slice a, td::Slice b);
static td::SecureString hmac_sha512(td::Slice key, td::Slice message);
static td::SecureString kdf(td::Slice secret, td::Slice password, int iterations);
static td::SecureString encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message, td::Slice secret);
static td::Result<td::SecureString> encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message, td::Slice secret);
static td::Result<td::SecureString> decrypt_header(td::Slice encrypted_header, td::Slice encrypted_message,
td::Slice secret);
private:
static td::AesCbcState calc_aes_cbc_state_from_hash(td::Slice hash);
static td::AesCbcState calc_aes_cbc_state_from_secret(td::Slice seed);
static td::SecureString gen_random_prefix(td::int64 data_size, td::int64 min_padding);
static td::SecureString gen_deterministic_prefix(td::int64 data_size, td::int64 min_padding);
static td::SecureString encrypt_data_with_prefix(td::Slice data, td::Slice secret);
static td::SecureString kdf_expand(td::Slice random_secret, td::Slice info);
friend class SimpleEncryptionV2;
friend class EncryptionTest;
};
} // namespace tde2e_core

View File

@ -173,7 +173,7 @@ std::vector<std::string> Mnemonic::generate_verification_words(td::Slice data) {
static auto bip_words = Mnemonic::normalize_and_split(td::SecureString(bip39_english()));
CHECK(bip_words.size() == BIP_WORD_COUNT);
auto hash = MessageEncryption::combine_secrets("MnemonicVerificationWords", data);
auto hash = MessageEncryption::hmac_sha512("MnemonicVerificationWords", data);
CHECK(hash.size() == HASH_SIZE);
std::vector<std::string> verification_words;

View File

@ -49,7 +49,7 @@ td::Result<td::SecureString> QRHandshakeBob::receive_accept(td::int64 alice_user
TRY_RESULT_ASSIGN(o_ephemeral_shared_secret_, bob_ephemeral_private_key_.compute_shared_secret(*o_alice_public_key_));
TRY_RESULT(shared_secret_tmp, bob_private_key_.compute_shared_secret(*o_alice_public_key_));
o_shared_secret_ = MessageEncryption::combine_secrets(o_ephemeral_shared_secret_.value(), shared_secret_tmp);
o_shared_secret_ = MessageEncryption::hmac_sha512(o_ephemeral_shared_secret_.value(), shared_secret_tmp);
TRY_RESULT(decrypted_accept, decrypt_ephemeral(encrypted_accept));
td::TlParser parser(decrypted_accept);
@ -136,7 +136,7 @@ td::Result<QRHandshakeAlice> QRHandshakeAlice::create(td::int64 alice_user_id, P
auto bob_ephemeral_public_key = PublicKey::from_u256(qr->bob_ephemeral_PK_);
TRY_RESULT(ephemeral_shared_secret, alice_private_key.compute_shared_secret(bob_ephemeral_public_key));
TRY_RESULT(shared_secret_tmp, alice_private_key.compute_shared_secret(bob_public_key));
auto shared_secret = MessageEncryption::combine_secrets(ephemeral_shared_secret, shared_secret_tmp);
auto shared_secret = MessageEncryption::hmac_sha512(ephemeral_shared_secret, shared_secret_tmp);
return QRHandshakeAlice{alice_user_id,
std::move(alice_private_key),
bob_user_id,

View File

@ -23,6 +23,9 @@
#include <utility>
int VERBOSITY_NAME(blkch) = VERBOSITY_NAME(INFO);
namespace tde2e_api {
Result<SecureBytes> call_export_shared_key(CallId call_id);
} // namespace tde2e_api
namespace tde2e_core {
// BlockchainLogger implementation
@ -64,6 +67,7 @@ void BlockchainLogger::log_try_apply_block(td::Slice block_slice, Height height,
log_file_ << "TRY_APPLY_BLOCK\n";
log_file_ << base64_encode(block_slice) << "\n";
log_file_ << base64_encode(Blockchain::from_local_to_server(block_slice.str()).move_as_ok()) << "\n";
log_file_ << height.height << "\n";
log_file_ << height.broadcast_height << "\n";
if (result.is_ok()) {
@ -80,6 +84,7 @@ void BlockchainLogger::log_try_apply_broadcast_block(td::Slice block_slice, Heig
log_file_ << "TRY_APPLY_BROADCAST_BLOCK\n";
log_file_ << base64_encode(block_slice) << "\n";
log_file_ << base64_encode(Blockchain::from_local_to_server(block_slice.str()).move_as_ok()) << "\n";
log_file_ << height.height << "\n";
log_file_ << height.broadcast_height << "\n";
if (result.is_ok()) {
@ -155,7 +160,11 @@ void BlockchainLogger::log_get_proof(td::int64 height, const std::vector<std::st
// ServerBlockchain implementation with logging
td::Status ServerBlockchain::try_apply_block(td::Slice block_slice) {
TRY_RESULT(block, Block::from_tl_serialized(block_slice));
auto status = blockchain_.try_apply_block(block);
ValidateOptions validate_options;
validate_options.permissions = GroupParticipantFlags::AllPermissions;
validate_options.validate_signature = true;
validate_options.validate_state_hash = true;
auto status = blockchain_.try_apply_block(block, validate_options);
if (status.is_ok()) {
blocks_.push_back(block);
broadcast_chain_.on_new_main_block(blockchain_);
@ -192,13 +201,13 @@ td::Result<std::string> ServerBlockchain::get_block(size_t height, int sub_chain
result = td::Status::Error(PSLICE() << "Invalid height " << height);
} else {
CHECK(blocks_[height].height_ == static_cast<td::int64>(height));
result = blocks_[height].to_tl_serialized();
result = Blockchain::from_local_to_server(blocks_[height].to_tl_serialized());
}
} else if (sub_chain == 1) {
if (height >= broadcast_blocks_.size()) {
result = td::Status::Error(PSLICE() << "Invalid height " << height);
} else {
result = broadcast_blocks_[height];
result = Blockchain::from_local_to_server(broadcast_blocks_[height]);
}
}
@ -362,7 +371,10 @@ BlockBuilder &BlockBuilder::skip_public_key() {
}
BlockBuilder &BlockBuilder::set_value_raw(td::Slice key, td::Slice value) {
block.changes_.push_back(Change{ChangeSetValue{key.str(), key.str()}});
kv_state_.set_value(key, value).ensure();
block.state_proof_.kv_hash = KeyValueHash{kv_state_.get_hash()};
block.changes_.push_back(Change{ChangeSetValue{key.str(), value.str()}});
has_hash_proof = true;
return *this;
}
@ -509,7 +521,7 @@ td::Result<std::vector<std::string>> BlockchainTester::get_values(const std::vec
return values;
}
td::Result<std::string> BlockchainTester::get_block(td::int64 height, int sub_chain) {
td::Result<std::string> BlockchainTester::get_block_from_server(td::int64 height, int sub_chain) {
return server_.get_block(static_cast<std::size_t>(height), sub_chain);
}
@ -548,7 +560,7 @@ td::Result<ApplyResult> BlockchainTester::apply(const Block &block, td::Slice bl
auto server_status = server_.try_apply_block(block_str);
auto client_status = client_.try_apply_block(block_str);
if (server_status.is_error() != client_status.is_error()) {
return td::Status::Error(PSLICE() << "Server and client return different answers:\n\tsever:" << server_status
return td::Status::Error(PSLICE() << "Server and client return different answers:\n\tserver:" << server_status
<< "\n\tclient:" << client_status);
}
if (server_status.is_error()) {
@ -718,8 +730,8 @@ td::Result<bool> CallTester::user_full_sync(User &user) {
td::Status CallTester::user_init_call(User &user) {
CHECK(user.call_id == 0);
CHECK(user.in_call);
TEST_TRY_RESULT(block, bt.get_block(++user.height.height));
TRY_RESULT(call_id, to_td(tde2e_api::call_create(user.private_key_id, block)));
TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.height));
TRY_RESULT(call_id, to_td(tde2e_api::call_create(user.user_id, user.private_key_id, block)));
user.call_id = call_id;
return td::Status::OK();
}
@ -737,7 +749,7 @@ td::Result<bool> CallTester::user_sync_chain_step(User &user) {
if (user.height.height == height.height) {
return false;
}
TEST_TRY_RESULT(block, bt.get_block(++user.height.height));
TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.height));
TEST_TRY_STATUS(to_td(tde2e_api::call_apply_block(user.call_id, block)));
return true;
}
@ -748,10 +760,10 @@ td::Result<bool> CallTester::user_sync_broadcast_step(User &user) {
if (user.height.broadcast_height == height.broadcast_height) {
return false;
}
TEST_TRY_RESULT(block, bt.get_block(++user.height.broadcast_height, 1));
TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.broadcast_height, 1));
auto r = tde2e_api::call_receive_inbound_message(user.call_id, block);
if (!r.is_ok()) {
return td::Status::Error("Failed to call apply broadcast");
return td::Status::Error(PSLICE() << "Failed to call apply broadcast: " << r.error().message);
}
return true;
}

View File

@ -140,6 +140,8 @@ struct BlockBuilder {
bool has_signature{false};
tde2e_core::Block block;
KeyValueState kv_state_;
void sign(const PrivateKey &private_key);
void zero_sign();
std::string hash_key(td::Slice key) const;
@ -162,7 +164,7 @@ struct BlockchainTester {
void reindex();
td::Result<std::vector<std::string>> get_values(const std::vector<std::string> &keys);
td::Result<std::string> get_block(td::int64 height, int sub_chain = 0);
td::Result<std::string> get_block_from_server(td::int64 height, int sub_chain = 0);
td::Result<std::string> get_value(td::Slice key);

View File

@ -31,29 +31,6 @@
#include <memory>
namespace tde2e_api {
ErrorCode to_error_code(td::int32) {
return ErrorCode::UnknownError;
}
template <class T>
Result<T> to_result(const td::Status &status) {
return Result<T>(Error{to_error_code(status.code()), status.message().str()});
}
template <class T>
Result<T> to_result(td::Result<T> &value) {
if (value.is_ok()) {
return Result<T>(value.move_as_ok());
}
return to_result<T>(value.error());
}
template <typename T>
Result<T>::Result(td::Result<T> &&value) : Result(to_result(value)) {
}
template <typename T>
Result<T>::Result(td::Status &&status) : Result(to_result<T>(status)) {
}
} // namespace tde2e_api
namespace tde2e_core {
@ -66,7 +43,7 @@ using StorageRef = UniqueRef<EncryptedStorage>;
using CallRef = UniqueRef<Call>;
td::UInt256 to_hash(td::Slice tag, td::Slice serialization) {
auto res = MessageEncryption::combine_secrets(tag, serialization);
auto res = MessageEncryption::hmac_sha512(tag, serialization);
td::UInt256 hash;
hash.as_mutable_slice().copy_from(res.as_slice().substr(0, 32));
return hash;
@ -74,6 +51,17 @@ td::UInt256 to_hash(td::Slice tag, td::Slice serialization) {
class KeyChain {
public:
KeyChain() {
set_log_verbosity_level(1).ignore();
}
td::Result<api::Ok> set_log_verbosity_level(td::int32 new_verbosity_level) {
if (0 <= new_verbosity_level && new_verbosity_level <= VERBOSITY_NAME(NEVER)) {
SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL) + new_verbosity_level);
return api::Ok{};
}
return td::Status::Error("Wrong new verbosity level specified");
}
td::Result<api::PrivateKeyId> generate_private_key() {
TRY_RESULT(mnemonic, Mnemonic::create_new({}));
return from_words(mnemonic.get_words_string());
@ -94,11 +82,12 @@ class KeyChain {
}
td::Result<api::SymmetricKeyId> derive_secret(api::PrivateKeyId key_id, td::Slice tag) {
TRY_RESULT(pk, to_private_key_with_memonic(key_id));
TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
auto hash = to_hash(PSLICE() << "derive secret with tag: " << td::base64_encode(tag),
pk.to_public_key().to_u256().as_slice());
return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> {
return MessageEncryption::combine_secrets(tag, pk.to_private_key().to_secure_string());
// TODO: this is probably wrong and should be changed
return MessageEncryption::hmac_sha512(pk.to_private_key().to_secure_string(), tag);
});
}
@ -112,7 +101,7 @@ class KeyChain {
}
td::Result<api::Bytes> to_encrypted_private_key(api::PrivateKeyId key_id, api::SymmetricKeyId secret_id) {
TRY_RESULT(pk, to_private_key_with_memonic(key_id));
TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
TRY_RESULT(secret, to_secret_ref(secret_id));
auto decrypted_key =
DecryptedKey(td::transform(pk.words(), [](const auto &m) { return m.copy(); }), pk.to_private_key());
@ -133,6 +122,23 @@ class KeyChain {
});
}
td::Result<api::Bytes> to_encrypted_private_key_internal(api::PrivateKeyId key_id, api::SymmetricKeyId secret_id) {
TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
TRY_RESULT(secret, to_secret_ref(secret_id));
return MessageEncryption::encrypt_data(pk.to_private_key().to_secure_string(), *secret).as_slice().str();
}
td::Result<api::PrivateKeyId> from_encrypted_private_key_internal(td::Slice encrypted_private_key,
api::SymmetricKeyId secret_id) {
TRY_RESULT(secret, to_secret_ref(secret_id));
auto hash = to_hash(PSLICE() << "encrypted private ed25519 key internal " << encrypted_private_key.str(), *secret);
return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
TRY_RESULT(raw_pk, MessageEncryption::decrypt_data(encrypted_private_key, *secret));
TRY_RESULT(pk, PrivateKey::from_slice(raw_pk));
return PrivateKeyWithMnemonic::from_private_key(pk, {});
});
}
td::Result<api::PublicKeyId> from_public_key(td::Slice public_key) {
TRY_RESULT(key, PublicKey::from_slice(public_key));
auto hash = to_hash("public ed25519 key", public_key);
@ -141,7 +147,7 @@ class KeyChain {
td::Result<api::SymmetricKeyId> from_ecdh(api::PrivateKeyId private_key_id, api::PublicKeyId public_key_id) {
TRY_RESULT(public_key, to_public_key(public_key_id));
TRY_RESULT(private_key, to_private_key_with_memonic(private_key_id));
TRY_RESULT(private_key, to_private_key_with_mnemonic(private_key_id));
auto hash = to_hash("x25519 shared secret",
public_key.to_u256().as_slice().str() + private_key.to_public_key().to_u256().as_slice().str());
return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> {
@ -155,7 +161,7 @@ class KeyChain {
return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> { return td::SecureString(secret); });
}
td::Result<api::SecureBytes> to_words(api::PrivateKeyId private_key_id) {
TRY_RESULT(private_key, to_private_key_with_memonic(private_key_id));
TRY_RESULT(private_key, to_private_key_with_mnemonic(private_key_id));
api::SecureBytes res;
auto words = private_key.words();
for (size_t i = 0; i < words.size(); ++i) {
@ -168,7 +174,7 @@ class KeyChain {
}
td::Result<api::Int512> sign(api::PrivateKeyId key, td::Slice data) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(key));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key));
TRY_RESULT(signature, private_key_ref.sign(td::Slice(data.data(), data.size())));
CHECK(signature.to_slice().size() == 64);
api::Int512 result;
@ -193,10 +199,9 @@ class KeyChain {
api::EncryptedMessageForMany res;
res.encrypted_message = MessageEncryption::encrypt_data(message, one_time_secret).as_slice().str();
for (auto &secret : secrets) {
res.encrypted_headers.emplace_back(
MessageEncryption::encrypt_header(one_time_secret, res.encrypted_message, secret->as_slice())
.as_slice()
.str());
TRY_RESULT(encrypted_header,
MessageEncryption::encrypt_header(one_time_secret, res.encrypted_message, secret->as_slice()));
res.encrypted_headers.emplace_back(encrypted_header.as_slice().str());
}
return res;
}
@ -214,8 +219,9 @@ class KeyChain {
api::EncryptedMessageForMany res;
for (auto &secret : secrets) {
res.encrypted_headers.emplace_back(
MessageEncryption::encrypt_header(header, secret->as_slice(), encrypted_message).as_slice().str());
TRY_RESULT(new_encrypted_header,
MessageEncryption::encrypt_header(header, secret->as_slice(), encrypted_message));
res.encrypted_headers.emplace_back(new_encrypted_header.as_slice().str());
}
return res;
}
@ -241,7 +247,7 @@ class KeyChain {
}
td::Result<api::HandshakeId> handshake_create_for_bob(api::UserId bob_user_id, api::PrivateKeyId bob_private_key_id) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(bob_private_key_id));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(bob_private_key_id));
return container_.try_build<Handshake>({}, [&]() -> td::Result<QRHandshakeBob> {
return QRHandshakeBob::create(bob_user_id, private_key_ref.to_private_key());
});
@ -254,7 +260,7 @@ class KeyChain {
api::PrivateKeyId alice_private_key_id,
api::UserId bob_user_id, td::Slice bob_public_key,
td::Slice start) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(alice_private_key_id));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(alice_private_key_id));
TRY_RESULT(bob_public_key_internal, PublicKey::from_slice(bob_public_key));
return container_.try_build<Handshake>({}, [&] {
return QRHandshakeAlice::create(alice_user_id, private_key_ref.to_private_key(), bob_user_id,
@ -339,7 +345,7 @@ class KeyChain {
return handshake_destroy({});
}
td::Result<api::StorageId> storage_create(api::PrivateKeyId key_id, td::Slice last_block) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(key_id));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key_id));
TRY_RESULT(storage, EncryptedStorage::create(last_block, private_key_ref.to_private_key()));
return container_.emplace<EncryptedStorage>(std::move(storage));
@ -364,7 +370,7 @@ class KeyChain {
}
template <class T>
td::Result<api::SignedEntry<T>> storage_sign_entry(api::PrivateKeyId key, api::Entry<T> entry) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(key));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key));
return EncryptedStorage::sign_entry(private_key_ref.to_private_key(), std::move(entry));
}
td::Result<std::optional<api::Contact>> storage_get_contact(api::StorageId storage_id, api::PublicKeyId key) {
@ -409,7 +415,7 @@ class KeyChain {
for (auto &participant : call_state.participants) {
TRY_RESULT(public_key, to_public_key(participant.public_key_id));
group_state.participants.push_back(
GroupParticipant{participant.user_id, participant.permissions & 3, public_key});
GroupParticipant{participant.user_id, participant.permissions & 3, public_key, 0});
}
return std::make_shared<GroupState>(std::move(group_state));
}
@ -424,24 +430,30 @@ class KeyChain {
}
td::Result<api::Bytes> call_create_zero_block(api::PrivateKeyId private_key_id, const api::CallState &initial_state) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(private_key_id));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
TRY_RESULT(group_state, to_group_state(initial_state));
return Call::create_zero_block(private_key_ref.to_private_key(), group_state);
}
tde2e_api::Result<std::string> call_create_self_add_block(api::PrivateKeyId private_key_id, td::Slice previous_block,
const tde2e_api::CallParticipant &self) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(private_key_id));
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
TRY_RESULT(public_key, to_public_key(self.public_key_id));
return Call::create_self_add_block(private_key_ref.to_private_key(), previous_block,
tde2e_core::GroupParticipant{self.user_id, 3, public_key});
tde2e_core::GroupParticipant{self.user_id, 3, public_key, 0});
}
td::Result<api::CallId> call_create(api::PrivateKeyId private_key_id, td::Slice last_block) {
TRY_RESULT(private_key_ref, to_private_key_with_memonic(private_key_id));
td::Result<api::CallId> call_create(api::UserId user_id, api::PrivateKeyId private_key_id, td::Slice last_block) {
TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
TRY_RESULT(call, Call::create(private_key_ref.to_private_key(), last_block));
TRY_RESULT(call, Call::create(user_id, private_key_ref.to_private_key(), last_block));
return container_.emplace<Call>(std::move(call));
}
td::Result<api::Bytes> call_describe(api::CallId call_id) {
TRY_RESULT(call_ref, to_call_ref(call_id));
td::StringBuilder sb;
sb << *call_ref;
return sb.as_cslice().str();
}
td::Result<api::Bytes> call_create_change_state_block(api::CallId call_id, const api::CallState &new_state) {
TRY_RESULT(call_ref, to_call_ref(call_id));
@ -450,15 +462,16 @@ class KeyChain {
}
td::Result<api::SecureBytes> call_export_shared_key(api::CallId call_id) {
TRY_RESULT(call_ref, to_call_ref(call_id));
return call_ref->shared_key().str();
TRY_RESULT(shared_key, call_ref->shared_key());
return shared_key.as_slice().str();
}
td::Result<api::Bytes> call_encrypt(api::CallId call_id, td::Slice message) {
td::Result<api::Bytes> call_encrypt(api::CallId call_id, api::CallChannelId channel_id, td::Slice message) {
TRY_RESULT(call_ref, to_call_ref(call_id));
return call_ref->encrypt(message);
return call_ref->encrypt(channel_id, message);
}
td::Result<api::SecureBytes> call_decrypt(api::CallId call_id, td::Slice message) {
td::Result<api::SecureBytes> call_decrypt(api::CallId call_id, api::UserId user_id, api::CallChannelId channel_id, td::Slice message) {
TRY_RESULT(call_ref, to_call_ref(call_id));
return call_ref->decrypt(message);
return call_ref->decrypt(user_id, channel_id, message);
}
td::Result<int> call_get_height(api::CallId call_id) {
@ -525,7 +538,7 @@ class KeyChain {
*key);
}
td::Result<PrivateKeyWithMnemonic> to_private_key_with_memonic(api::AnyKeyId key_id) const {
td::Result<PrivateKeyWithMnemonic> to_private_key_with_mnemonic(api::AnyKeyId key_id) const {
TRY_RESULT(key, container_.get_shared<Key>(key_id));
TRY_RESULT(ref, convert<PrivateKeyWithMnemonic>(std::move(key)));
return *ref;
@ -564,6 +577,9 @@ td::Slice to_slice(std::string_view s) {
}
return td::Slice(s.data(), s.size());
}
Result<Ok> set_log_verbosity_level(int new_verbosity_level) {
return get_default_keychain().set_log_verbosity_level(new_verbosity_level);
}
Result<PrivateKeyId> key_generate_private_key() {
return get_default_keychain().generate_private_key();
}
@ -576,12 +592,17 @@ Result<PrivateKeyId> key_derive_secret(PrivateKeyId key_id, Slice tag) {
Result<Bytes> key_to_encrypted_private_key(PrivateKeyId key_id, SymmetricKeyId secret_id) {
return get_default_keychain().to_encrypted_private_key(key_id, secret_id);
}
Result<PrivateKeyId> key_from_encrypted_private_key(Slice encrypted_key, SymmetricKeyId secret_id) {
return get_default_keychain().from_encrypted_private_key(to_slice(encrypted_key), secret_id);
}
Result<SymmetricKeyId> key_from_bytes(SecureSlice secret) {
return get_default_keychain().from_bytes(to_slice(secret));
}
Result<PrivateKeyId> key_from_encrypted_private_key(Slice encrypted_key, SymmetricKeyId secret_id) {
return get_default_keychain().from_encrypted_private_key(to_slice(encrypted_key), secret_id);
Result<Bytes> key_to_encrypted_private_key_internal(PrivateKeyId key_id, SymmetricKeyId secret_id) {
return get_default_keychain().to_encrypted_private_key_internal(key_id, secret_id);
}
Result<PrivateKeyId> key_from_encrypted_private_key_internal(Slice encrypted_key, SymmetricKeyId secret_id) {
return get_default_keychain().from_encrypted_private_key_internal(to_slice(encrypted_key), secret_id);
}
Result<PublicKeyId> key_from_public_key(Slice public_key) {
@ -733,11 +754,16 @@ Result<Bytes> call_create_self_add_block(PrivateKeyId private_key_id, Slice prev
const CallParticipant &self) {
return get_default_keychain().call_create_self_add_block(private_key_id, to_slice(previous_block), self);
}
Result<CallId> call_create(PrivateKeyId private_key_id, Slice last_block) {
return get_default_keychain().call_create(private_key_id, to_slice(last_block));
Result<CallId> call_create(UserId user_id, PrivateKeyId private_key_id, Slice last_block) {
return get_default_keychain().call_create(user_id, private_key_id, to_slice(last_block));
}
Result<std::string> call_describe(CallId call_id) {
return get_default_keychain().call_describe(call_id);
}
Result<std::string> call_describe_block(Slice block_slice) {
td::TlParser parser(to_slice(block_slice));
bool is_server = tde2e_core::Blockchain::is_from_server(to_slice(block_slice));
TRY_RESULT(block_str, tde2e_core::Blockchain::from_any_to_local(std::string(block_slice)));
td::TlParser parser(block_str);
auto magic = parser.fetch_int();
if (magic != td::e2e_api::e2e_chain_block::ID) {
return td::Status::Error("wrong magic");
@ -745,14 +771,17 @@ Result<std::string> call_describe_block(Slice block_slice) {
auto block = td::e2e_api::e2e_chain_block::fetch(parser);
parser.fetch_end();
TRY_STATUS(parser.get_status());
return to_string(block);
return PSTRING() << (is_server ? "Server:" : "Local:") << to_string(block);
}
Result<std::string> call_describe_message(Slice broadcast_slice) {
td::TlParser parser(to_slice(broadcast_slice));
bool is_server = tde2e_core::Blockchain::is_from_server(to_slice(broadcast_slice));
TRY_RESULT(broadcast_str, tde2e_core::Blockchain::from_any_to_local(std::string(broadcast_slice)));
td::TlParser parser(broadcast_str);
auto broadcast = td::e2e_api::e2e_chain_GroupBroadcast::fetch(parser);
parser.fetch_end();
TRY_STATUS(parser.get_status());
return to_string(broadcast);
return PSTRING() << (is_server ? "Server:" : "Local:") << to_string(broadcast);
}
Result<Bytes> call_create_change_state_block(CallId call_id, const CallState &new_state) {
return get_default_keychain().call_create_change_state_block(call_id, new_state);
@ -760,11 +789,11 @@ Result<Bytes> call_create_change_state_block(CallId call_id, const CallState &ne
Result<SecureBytes> call_export_shared_key(CallId call_id) {
return get_default_keychain().call_export_shared_key(call_id);
}
Result<Bytes> call_encrypt(CallId call_id, SecureSlice message) {
return get_default_keychain().call_encrypt(call_id, to_slice(message));
Result<Bytes> call_encrypt(CallId call_id, CallChannelId channel_id, SecureSlice message) {
return get_default_keychain().call_encrypt(call_id, channel_id, to_slice(message));
}
Result<SecureBytes> call_decrypt(CallId call_id, Slice message) {
return get_default_keychain().call_decrypt(call_id, to_slice(message));
Result<SecureBytes> call_decrypt(CallId call_id, UserId user_id, CallChannelId channel_id, Slice message) {
return get_default_keychain().call_decrypt(call_id, user_id, channel_id, to_slice(message));
}
Result<int> call_get_height(CallId call_id) {
return get_default_keychain().call_get_height(call_id);

View File

@ -98,6 +98,8 @@ struct EncryptedMessageForMany {
std::string encrypted_message;
};
Result<Ok> set_log_verbosity_level(int level);
// Keys management
// private keys will stay inside the library when it is possible
// all keys are stored only in memory and should be created or imported before usage
@ -116,6 +118,10 @@ Result<Int512> key_sign(PrivateKeyId key, Slice data);
Result<Ok> key_destroy(AnyKeyId key_id);
Result<Ok> key_destroy_all();
// Used to encrypt key between processes, secret_id must be generated with key_from_ecdh
Result<Bytes> key_to_encrypted_private_key_internal(PrivateKeyId key_id, SymmetricKeyId secret_id);
Result<PrivateKeyId> key_from_encrypted_private_key_internal(Slice encrypted_key, SymmetricKeyId secret_id);
Result<EncryptedMessageForMany> encrypt_message_for_many(const std::vector<SymmetricKeyId> &key_ids,
SecureSlice message);
// keeps encrypted_message empty in result
@ -168,6 +174,7 @@ Result<PrivateKeyId> login_finish_for_bob(LoginId bob_login_id, UserId alice_use
Result<Ok> login_destroy(LoginId login_id);
Result<Ok> login_destroy_all();
// Personal info
// TODO: UserId
@ -276,6 +283,7 @@ Result<Ok> storage_blockchain_add_proof(StorageId storage_id, Slice proof, const
Result<StorageBlockchainState> storage_get_blockchain_state(StorageId);
using CallId = std::int64_t;
using CallChannelId = std::int32_t;
struct CallParticipant {
UserId user_id;
PublicKeyId public_key_id;
@ -289,15 +297,15 @@ struct CallState {
Result<Bytes> call_create_zero_block(PrivateKeyId private_key_id, const CallState &initial_state);
Result<Bytes> call_create_self_add_block(PrivateKeyId private_key_id, Slice previous_block, const CallParticipant &self);
Result<CallId> call_create(PrivateKeyId private_key_id, Slice last_block);
Result<CallId> call_create(UserId user_id, PrivateKeyId private_key_id, Slice last_block);
Result<std::string> call_describe(CallId call);
Result<std::string> call_describe_block(Slice block);
Result<std::string> call_describe_message(Slice message);
Result<Bytes> call_create_change_state_block(CallId call_id, const CallState &new_state);
Result<SecureBytes> call_export_shared_key(CallId call_id);
Result<Bytes> call_encrypt(CallId call_id, SecureSlice message);
Result<SecureBytes> call_decrypt(CallId call_id, Slice message);
Result<Bytes> call_encrypt(CallId call_id, CallChannelId channel_id, SecureSlice message);
Result<SecureBytes> call_decrypt(CallId call_id, UserId user_id, CallChannelId channel_id, Slice message);
Result<int> call_get_height(CallId call_id);
Result<CallState> call_apply_block(CallId call_id, Slice block);
@ -324,32 +332,4 @@ struct CallVerificationWords {
Result<CallVerificationWords> call_get_verification_words(CallId call_id);
Result<Ok> call_destroy(CallId call_id);
Result<Ok> call_destroy_all();
// TODO: pull values from blockchain state
// 1. we have a lot of contacts with information about contacts
// 2. PublicKey -> info
// 3. UserId -> List<PublicKey>
// 4. api should check all signatures before saving data
// 5. it should also handle timestamp in signature to apply the latest info
// 6. Changes will be synchronized with blockchain
// 7. blockchain logic will be mostly hidden
// 1. One must synchronize state of blockchain
// 2. One may need to get proofs for keys from the server
// 3. One must save changes and handle races
// interesting moment - we may speculatively return new data if signature is checked
// But it becomes complicated, when we are talking about public key itself.
// one MUST NOT use public key in any way before it is saved into blockchain
// It is more or less OK do decrypt.
// It is NOT OK to encrypt.
// Case I am worried about - sever may send public keys of user and then it may refuse to save it into blockchain.
// What it means - we must persist public key before it is used
// TODO: maybe we could forbid usage of unchecked public keys?
//
// TODO: backup OR recovery key?
// Maybe we shouldn't keep the key on device AT ALL?
//
} // namespace tde2e_api

View File

@ -11,21 +11,33 @@
namespace tde2e_api {
enum class ErrorCode : int {
UnknownError = 1,
Any,
InvalidInput,
InvalidKeyId,
InvalidId,
InvalidBlock,
InvalidBlock_InvalidSignature,
InvalidBlock_HashMismatch,
InvalidBlock_HeightMismatch,
InvalidBlock_InvalidStateProof_Group,
InvalidBlock_InvalidStateProof_Secret,
InvalidBlock_NoPermissions,
InvalidBlock_InvalidGroupState,
Decrypt_UnknownEpoch,
Encrypt_UnknownEpoch,
UnknownError = 100,
Any = 101,
InvalidInput = 102,
InvalidKeyId = 103,
InvalidId = 104,
InvalidBlock = 200,
InvalidBlock_NoChanges = 201,
InvalidBlock_InvalidSignature = 202,
InvalidBlock_HashMismatch = 203,
InvalidBlock_HeightMismatch = 204,
InvalidBlock_InvalidStateProof_Group = 205,
InvalidBlock_InvalidStateProof_Secret = 206,
InvalidBlock_NoPermissions = 207,
InvalidBlock_InvalidGroupState = 208,
InvalidCallGroupState_NotParticipant = 300,
InvalidCallGroupState_WrongUserId = 301,
Decrypt_UnknownEpoch = 400,
Encrypt_UnknownEpoch = 401,
InvalidBroadcast_InFuture = 500,
InvalidBroadcast_NotInCommit = 501,
InvalidBroadcast_NotInReveal = 502,
InvalidBroadcast_UnknownUserId = 503,
InvalidBroadcast_AlreadyApplied = 504,
InvalidBroadcast_InvalidReveal = 505,
InvalidBroadcast_InvalidBlockHash = 506,
InvalidCallChannelId = 600,
CallFailed = 601
};
inline std::string_view error_string(ErrorCode error_code) {
switch (error_code) {
@ -41,6 +53,8 @@ inline std::string_view error_string(ErrorCode error_code) {
return "INVALID_ID";
case ErrorCode::InvalidBlock:
return "INVALID_BLOCK";
case ErrorCode::InvalidBlock_NoChanges:
return "INVALID_BLOCK__NO_CHANGES";
case ErrorCode::InvalidBlock_InvalidSignature:
return "INVALID_BLOCK__INVALID_SIGNATURE";
case ErrorCode::InvalidBlock_HashMismatch:
@ -55,10 +69,32 @@ inline std::string_view error_string(ErrorCode error_code) {
return "INVALID_BLOCK__INVALID_GROUP_STATE";
case ErrorCode::InvalidBlock_NoPermissions:
return "INVALID_BLOCK__NO_PERMISSIONS";
case ErrorCode::InvalidCallGroupState_NotParticipant:
return "INVALID_CALL_GROUP_STATE__NOT_PARTICIPANT";
case ErrorCode::InvalidCallGroupState_WrongUserId:
return "INVALID_CALL_GROUP_STATE__WRONG_USER_ID";
case ErrorCode::Decrypt_UnknownEpoch:
return "DECRYPT__UNKNOWN_EPOCH";
case ErrorCode::Encrypt_UnknownEpoch:
return "ENCRYPT__UNKNOWN_EPOCH";
case ErrorCode::InvalidBroadcast_InFuture:
return "INVALID_BROADCAST__IN_FUTURE";
case ErrorCode::InvalidBroadcast_NotInCommit:
return "INVALID_BROADCAST__NOT_IN_COMMIT";
case ErrorCode::InvalidBroadcast_NotInReveal:
return "INVALID_BROADCAST__NOT_IN_REVEAL";
case ErrorCode::InvalidBroadcast_UnknownUserId:
return "INVALID_BROADCAST__UNKNOWN_USER_ID";
case ErrorCode::InvalidBroadcast_AlreadyApplied:
return "INVALID_BROADCAST__ALREADY_APPLIED";
case ErrorCode::InvalidBroadcast_InvalidReveal:
return "INVALID_BROADCAST__INVALID_REVEAL";
case ErrorCode::InvalidBroadcast_InvalidBlockHash:
return "INVALID_BROADCAST__INVALID_BLOCK_HASH";
case ErrorCode::CallFailed:
return "CALL_FAILED";
case ErrorCode::InvalidCallChannelId:
return "INVALID_CALL_CHANNEL_ID";
}
return "UNKNOWN_ERROR";
}

View File

@ -0,0 +1,279 @@
import os
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, HMAC, SHA512
import binascii
import random
def generate_deterministic_padding(data_size, min_padding):
# Calculate padding size to make total size multiple of 16
padding_size = ((min_padding + 15 + data_size) & -16) - data_size
padding = bytearray(padding_size)
# Only set the first byte to padding size, leave rest as zeros
padding[0] = padding_size
return padding
def hmac_sha512(a, b):
# Combine two secrets using HMAC-SHA512
if isinstance(a, str):
a = a.encode('utf-8')
if isinstance(b, str):
b = b.encode('utf-8')
hmac = HMAC.new(a, b, SHA512)
return hmac.digest()
def kdf(key, info):
return hmac_sha512(key, info)
def encrypt_data_with_prefix(data, secret):
# Ensure data is multiple of 16 bytes
assert len(data) % 16 == 0
# Generate encryption and HMAC secrets
large_secret = kdf(secret, "tde2e_encrypt_data")
encrypt_secret = large_secret[:32]
hmac_secret = large_secret[32:64]
# Generate message ID using HMAC
large_msg_id = hmac_sha512(hmac_secret, data)
msg_id = large_msg_id[:16] # Use first 16 bytes as message ID
# Create result buffer
result = bytearray(len(data) + 16)
result[0:16] = msg_id
# Generate key and IV for encryption
encryption_secret = hmac_sha512(encrypt_secret, msg_id)
key = encryption_secret[:32]
iv = encryption_secret[32:48]
# Encrypt data
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(data)
result[16:] = encrypted
return bytes(result)
def encrypt_data_with_deterministic_padding(data, secret):
# Generate deterministic padding
padding = generate_deterministic_padding(len(data), 16)
# Combine padding and data
combined = bytearray(len(padding) + len(data))
combined[0:len(padding)] = padding
combined[len(padding):] = data
# Encrypt the combined data
return encrypt_data_with_prefix(combined, secret)
def encrypt_header(header, encrypted_message, secret):
# Verify inputs
assert len(header) == 32
assert len(encrypted_message) >= 16
# Get msg_id from the beginning of encrypted message
msg_id = encrypted_message[0:16]
encryption_key = kdf(secret, "tde2e_encrypt_header")[:32]
# Generate encryption key and IV from secret and message ID
encryption_secret = kdf(encryption_key, msg_id)
key = encryption_secret[:32]
iv = encryption_secret[32:48]
# Encrypt header with AES-CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted_header = cipher.encrypt(header)
return encrypted_header
def decrypt_data(encrypted_data, secret):
# Verify input size
if len(encrypted_data) < 17:
raise ValueError("Failed to decrypt: data is too small")
if len(encrypted_data) % 16 != 0:
raise ValueError("Failed to decrypt: data size is not divisible by 16")
# Extract msg_id and encrypted part
msg_id = encrypted_data[0:16]
encrypted_part = encrypted_data[16:]
# Generate encryption and HMAC secrets
large_secret = kdf(secret, "tde2e_encrypt_data")
hmac_secret = large_secret[32:64]
encrypt_secret = large_secret[:32]
# Generate key and IV for decryption
encryption_secret = hmac_sha512(encrypt_secret, msg_id)
key = encryption_secret[:32]
iv = encryption_secret[32:48]
# Decrypt with AES-CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = cipher.decrypt(encrypted_part)
# Verify msg_id
large_msg_id = hmac_sha512(hmac_secret, decrypted_data)
expected_msg_id = large_msg_id[:16]
if msg_id != expected_msg_id:
raise ValueError("Failed to decrypt: msg_id mismatch")
# Extract actual data by removing padding
prefix_size = decrypted_data[0]
if prefix_size > len(decrypted_data) or prefix_size < 16:
raise ValueError("Failed to decrypt: invalid prefix size")
return decrypted_data[prefix_size:]
def decrypt_header(encrypted_header, encrypted_message, secret):
# Verify inputs
if len(encrypted_header) != 32:
raise ValueError("Failed to decrypt: invalid header size")
if len(encrypted_message) < 16:
raise ValueError("Failed to decrypt: invalid message size")
# Get msg_id from the beginning of encrypted message
msg_id = encrypted_message[0:16]
encryption_key = kdf(secret, "tde2e_encrypt_header")[:32]
# Generate encryption key and IV from secret and msg_id
encryption_secret = kdf(encryption_key, msg_id)
key = encryption_secret[:32]
iv = encryption_secret[32:48]
# Decrypt header with AES-CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_header = cipher.decrypt(encrypted_header)
return decrypted_header
def generate_random_bytes(length):
return bytes(random.getrandbits(8) for _ in range(length))
def generate_test_vectors():
# Generate random secrets and headers for each test
secret = generate_random_bytes(32)
header = generate_random_bytes(32)
# Test vectors with different data patterns
test_vectors = [
{
"name": "empty_message",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": "",
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "simple_message",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify(b"Hello, World!").decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "long_message",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify(b"x" * 200).decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "random_message",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify(generate_random_bytes(64)).decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "very_long_message",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify(generate_random_bytes(300)).decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "message_with_special_chars",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify(bytes([i for i in range(33, 64)])).decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
{
"name": "message_with_unicode",
"secret": binascii.hexlify(secret).decode('ascii'),
"data": binascii.hexlify("Hello, 世界!".encode('utf-8')).decode('ascii'),
"header": binascii.hexlify(header).decode('ascii')
},
]
# Generate encrypted data and headers for each test vector
for vec in test_vectors:
secret = binascii.unhexlify(vec["secret"])
data = binascii.unhexlify(vec["data"])
header = binascii.unhexlify(vec["header"])
encrypted = encrypt_data_with_deterministic_padding(data, secret)
encrypted_header = encrypt_header(header, encrypted, secret)
# Test decryption
decrypted = decrypt_data(encrypted, secret)
if decrypted != data:
raise ValueError(f"Decryption failed for test vector: {vec['name']}")
# Test header decryption
decrypted_header = decrypt_header(encrypted_header, encrypted, secret)
if decrypted_header != header:
raise ValueError(f"Header decryption failed for test vector: {vec['name']}")
vec["encrypted"] = binascii.hexlify(encrypted).decode('ascii')
vec["encrypted_header"] = binascii.hexlify(encrypted_header).decode('ascii')
return test_vectors
def print_cpp_header(test_vectors):
print("#pragma once")
print("\n#include <string>")
print("#include <vector>")
print("\nnamespace tde2e_core {")
print("\nstruct TestVector {")
print(" std::string name;")
print(" std::string secret;")
print(" std::string data;")
print(" std::string header;")
print(" std::string encrypted;")
print(" std::string encrypted_header;")
print("};")
print("\ninline std::vector<TestVector> get_test_vectors() {")
print(" return {")
for vec in test_vectors:
print(" {")
print(f' "{vec["name"]}",')
print(f' "{vec["secret"]}",')
print(f' "{vec["data"]}",')
print(f' "{vec["header"]}",')
print(f' "{vec["encrypted"]}",')
print(f' "{vec["encrypted_header"]}"')
print(" },")
print(" };")
print("}")
print("\n} // namespace tde2e_core")
if __name__ == "__main__":
test_vectors = generate_test_vectors()
# Get the directory of the current script
script_dir = os.path.dirname(os.path.abspath(__file__))
# Go up two levels to reach the tde2e directory
tde2e_dir = os.path.dirname(os.path.dirname(script_dir))
# Create the test directory if it doesn't exist
test_dir = os.path.join(tde2e_dir, "test")
os.makedirs(test_dir, exist_ok=True)
# Write the header file
header_path = os.path.join(test_dir, "EncryptionTestVectors.h")
with open(header_path, "w") as f:
# Redirect print output to the file
import sys
old_stdout = sys.stdout
sys.stdout = f
print_cpp_header(test_vectors)
sys.stdout = old_stdout

View File

@ -23,6 +23,31 @@
#include <string>
#include <utility>
namespace tde2e_api {
inline Error to_error(const td::Status &status) {
auto error_code = ErrorCode(status.code());
if (error_string(error_code) == "UNKNOWN_ERROR") {
return Error{ErrorCode::UnknownError, status.message().str()};
}
return Error{error_code, status.message().str()};
}
template <class T>
Result<T> to_result(td::Result<T> &value) {
if (value.is_ok()) {
return Result<T>(value.move_as_ok());
}
return Result<T>(to_error(value.error()));
}
template <typename T>
Result<T>::Result(td::Result<T> &&value) : Result(to_result(value)) {
}
template <typename T>
Result<T>::Result(td::Status &&status) : Result(to_error(status)) {
}
} // namespace tde2e_api
namespace tde2e_core {
using E = tde2e_api::ErrorCode;

View File

@ -0,0 +1,78 @@
#pragma once
#include <string>
#include <vector>
namespace tde2e_core {
struct TestVector {
std::string name;
std::string secret;
std::string data;
std::string header;
std::string encrypted;
std::string encrypted_header;
};
inline std::vector<TestVector> get_test_vectors() {
return {
{
"empty_message",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"9e2476ad849d22a44d9135c5c3c5e8b52d4f88473ae8745f3a9cec4d54780caf",
"de3539a5e10b20a3a0cffc24dbbd76b3a7e0eeab402cb38396d64785a3ab7c25"
},
{
"simple_message",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"48656c6c6f2c20576f726c6421",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"872b141f6d1e3554ead471dffd0fee5c04a0b04260eafcca9187158ce84c4487e9429df876706753913de61029402478",
"013037e02dc8dbf13598d96eb333a69a930efe043bac7dce0d6edfd1abc6bd2f"
},
{
"long_message",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"7878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"0fd497802c755b9c6fb9d58d38b9fb5fbd7dc9ccf78af3eb003a22d46bfa38894135d976bbec0f3cadc2b61a4d8648120dada26b2c3153def20fb9e370def31c802e202846946b5cb1bc2c01a7b46605292d6043ffb8f4040aaf18914e1c93fe9e683f088e23ee5e1551f00068a23fa3ebb8d6b9dcbf7a9072b12323b1a64247ba9bc7d277b08cfcc37387a0d24afca170dc027d8f0212eb62bccd9555de98936047c9bfc6a03aa539073f458795bd94b9b43003fe2299805f90c1d30ca631c8054242687e41e890bf4d744b529d7e96ea48a5bcec700993b5e980173049cf9df6f93d62ccc06d933fd6d7063890fa2d",
"697558ba60e789622ee90dffc55f1f1148cfea568c573b257fcf2083cf8aad0a"
},
{
"random_message",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"e61d2b05302c49faa9fac6a893957a846a2b30801dc171cf62ffec92297b10bfc4a82445839ecb4c5800ffb37c5356d4b95fc565ddb2e7f3e21f2936a952373c",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"515f0223f27302ca5e952ec978ec66bcdf04e7f72ae3a8e011e21457b355891d12e158c9b2dfb921520a0e5f531e6a20e95d42635b084a0b38a6e658f4a4181f85ea83756d316cea538cc34592491eef3e3530c34c63a693e3372cbdd0076628",
"b5bdfe6a3400c5d299d94756af4c18bbb8cca4a2635beeba3d89bbaa0025d9d3"
},
{
"very_long_message",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"de6e0dbde23e04b6be3d4c46868a1171c6b879227b19e370765823390501b195783c356af2156e828e511473d5698c9bd1cb4a70b7e209bbb02f8dda044af02ab6869478b211e3a17d72fbe6289c1b2e6132cb141e89cf72cf70defd67a23fbfe8e718a09a9c6a345565ebdc73e1f59927744801eb4e6f0b30d2705ea181e02a4030252330ac73bc4a4d51d2ccafb2f3f62abe3e81163be325ac823571c8dfae739f70bff39164e3cec7f53b88f97735dc25ac0c0630b1b41a131a979ba5164ab92e103716e9096e2fb5a6434b31d2c3673fca7e54dcdcce3807bfd43ee7bc3422868094305a9847ce7666bf57e49fb3c2009cb30eb3ba955942b923ec2c2a4e0341d86b524b198974bdce9cf863ab3526e9e03e53399dad20fc218554567c440536a31e05573f4cb930ba6a",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"e296a98cd96154b27d4b94cc520a8f028c61bd5ea69d22f0c3c13658c4bd5db0bd6661d0d17fac791c5cc06b3a54a853916bd7bf8a00644acc53cca43b6f51c66bc6a6cb98cf8d9f23dcd639089d1d9d3dfbc8829a1a81638317bbd3edb070c1dae181d97605eb42a6111b8696a16cc3e42639e38e93f872111fd67e934740f73a57df0e6edc6726c9aff99682bfcb7ddd99a3bef30da70d3c21e590fd02defc23be9f7f243e45a56b13562b8ecea09a14ac5af3a0500cb52f73f1bffea4a6624644da0bc4d112e5ee684b13a2ae8dbf401a5a8e581295a9dc876eaeb8ae4d732fb78d50a92c302d15c0a2308e43fc6e147ec162b28a534d6c95a2020fa141f3ce7f7dd25ff000d35f732a145abf31b3ff4d0da015d39da3b0fc70e692b567a9507be59e8c91a63fe809c495d76e70ec857cc24978771fc9314251e2bc4b1b24e0448514f97a6d438255cba8c1854019",
"8dab56b50ce6a4315b6e0b4eb32c39dfe817cb462ee9e070e2a60bb843c16107"
},
{
"message_with_special_chars",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"2014d1125081316a334896cbe5aed251d9fba6f3422afbc7e7bc9019e10e184241d18f71ef86603b5ce03e1c351e3dfff8adfb8f3498300a712ec134b367c533",
"6f5a5de410db3606ff94fa14d0e084a6ba5a51241a2abf45ac593e4748c477ce"
},
{
"message_with_unicode",
"5a08a19b447df98136a4502e01b286011b2d148084a7ca17e3a93616d279eb2a",
"48656c6c6f2c20e4b896e7958c21",
"a36c57ad5e8d6a30e80e010ab903b60da0206db1b4fd981cd61e059bbd8c0d4f",
"7ae70cb905f23109477b4d758d3907238ff4c37e2f351f086268ba3e85cef0257af58a70d8838c7b9c044f30382c2ccf",
"7dfb2d2e76df39b2fafdf01de009088a7e4d045b8630941986111ef2010d7c4a"
},
};
}
} // namespace tde2e_core

View File

@ -30,6 +30,7 @@ S_TEST(BlockchainValidation, ZeroBlock) {
TEST_DEBUG_VALUE(description, "Valid: zero block with group state only in proof");
auto block = BB().with_height(0)
.with_block_hash({})
.set_value("a", "b") // need some changes
.with_group_state({}, false, true, 7)
.with_shared_key({}, false, true)
.build(alice_pk);
@ -68,8 +69,12 @@ S_TEST(BlockchainValidation, ZeroBlock) {
}
{
TEST_DEBUG_VALUE(description, "Invalid: zero block with skipped group state proof");
auto block =
BB().with_height(0).with_block_hash({}).skip_group_state_proof().skip_shared_key_proof().build(alice_pk);
auto block = BB().with_height(0)
.set_value("a", "b")
.with_block_hash({})
.skip_group_state_proof()
.skip_shared_key_proof()
.build(alice_pk);
TEST_DEBUG_VALUE(block, block);
TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidStateProof_Group, block));
}
@ -77,6 +82,7 @@ S_TEST(BlockchainValidation, ZeroBlock) {
TEST_DEBUG_VALUE(description, "Invalid: zero block with wrong user_id in group state proof");
auto block = BB().with_height(0)
.with_block_hash({})
.set_value("a", "b")
.with_group_state({{1, 3, alice_pk.to_public_key()}}, false, true)
.skip_shared_key_proof()
.build(alice_pk);

View File

@ -43,6 +43,42 @@
using namespace tde2e_core;
namespace api = tde2e_api;
template <class T>
td::Status expect_error(td::Result<T> got) {
if (got.is_ok()) {
return td::Status::Error("Got Ok, instead of Error");
}
return td::Status::OK();
}
S_TEST(MessageEncryption, simple) {
std::string secret = "secret";
{
std::string data = "some private data";
std::string wrong_secret = "wrong secret";
auto encrypted_data = MessageEncryption::encrypt_data(data, secret);
LOG(ERROR) << encrypted_data.size();
TEST_TRY_RESULT(decrypted_data, MessageEncryption::decrypt_data(encrypted_data, secret));
TEST_ASSERT_EQ(data, decrypted_data, "decryption");
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(encrypted_data, wrong_secret)));
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data("", secret)));
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(32, 'a'), secret)));
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(33, 'a'), secret)));
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(64, 'a'), secret)));
TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(128, 'a'), secret)));
}
td::Random::Xorshift128plus rnd(123);
for (size_t i = 0; i < 255; i++) {
std::string data;
for (size_t j = 0; j < i; j++) {
data += static_cast<char>(rnd.fast('a', 'z'));
}
auto encrypted_data = MessageEncryption::encrypt_data(data, secret);
TEST_TRY_RESULT(decrypted_data, MessageEncryption::decrypt_data(encrypted_data, secret));
TEST_ASSERT_EQ(data, decrypted_data, "decryption");
}
return td::Status::OK();
}
struct E2eHandshakeTest {
td::Ed25519::PrivateKey alice;
@ -141,12 +177,12 @@ TEST(MiniBlockchain, Basic) {
Blockchain remote_blockchain = Blockchain::create_empty();
Blockchain local_blockchain = Blockchain::create_empty();
auto block = local_blockchain.set_value("a", "b", private_key);
remote_blockchain.try_apply_block(block).ensure();
local_blockchain.try_apply_block(block).ensure();
block = local_blockchain.set_value("b", "c", private_key);
remote_blockchain.try_apply_block(block).ensure();
local_blockchain.try_apply_block(block).ensure();
auto block = local_blockchain.set_value(std::string(32, 'a'), "b", private_key);
remote_blockchain.try_apply_block(block, {}).ensure();
local_blockchain.try_apply_block(block, {}).ensure();
block = local_blockchain.set_value(std::string(32, 'b'), "c", private_key);
remote_blockchain.try_apply_block(block, {}).ensure();
local_blockchain.try_apply_block(block, {}).ensure();
}
// Example usage
@ -674,6 +710,12 @@ S_TEST(E2E_Blockchain, Call) {
TEST(Call, Basic_API) {
using namespace tde2e_api;
auto F = [](Result<std::string> block) -> Result<std::string> {
if (block.is_ok()) {
return Blockchain::from_local_to_server(block.value());
}
return block;
};
auto key0 = key_generate_temporary_private_key().value();
auto pkey0 = key_from_public_key(key_to_public_key(key0).value()).value();
@ -684,22 +726,22 @@ TEST(Call, Basic_API) {
auto key3 = key_generate_temporary_private_key().value();
auto pkey3 = key_from_public_key(key_to_public_key(key3).value()).value();
auto zero_block = call_create_zero_block(key0, CallState{0, {CallParticipant{1, pkey0, 3}}}).value();
auto zero_block = F(call_create_zero_block(key0, CallState{0, {CallParticipant{-1, pkey0, 3}}})).value();
auto call1 = call_create(key0, zero_block).value();
auto block0 = call_create_self_add_block(key1, zero_block, CallParticipant{1, pkey1, 3}).value();
call1 = call_create(key1, block0).value();
auto call1 = call_create(-1, key0, zero_block).value();
auto block0 = F(call_create_self_add_block(key1, zero_block, CallParticipant{1, pkey1, 3})).value();
call1 = call_create(1, key1, block0).value();
auto block1 = call_create_self_add_block(key2, block0, CallParticipant{2, pkey2, 3}).value();
auto block1 = F(call_create_self_add_block(key2, block0, CallParticipant{2, pkey2, 3})).value();
call_apply_block(call1, block1).value();
auto call2 = call_create(key2, block1).value();
auto call2 = call_create(2, key2, block1).value();
ASSERT_EQ(call_get_verification_words(call2).value().words, call_get_verification_words(call1).value().words);
auto block2 =
call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}})
F(call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
.value();
call_describe_block(block2).value();
auto call3 = call_create(key3, block2).value();
auto call3 = call_create(3, key3, block2).value();
call_apply_block(call2, block2).value();
CHECK(!call_apply_block(call1, block2).is_ok());
@ -708,11 +750,11 @@ TEST(Call, Basic_API) {
ASSERT_EQ(call_get_verification_words(call2).value().words, call_get_verification_words(call3).value().words);
auto block31 =
call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}})
F(call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
.value();
call_apply_block(call2, block31).value();
auto commit2 = call_pull_outbound_messages(call2).value().at(0);
auto commit2 = F(call_pull_outbound_messages(call2).value().at(0)).value();
call_describe_message(commit2).value();
@ -720,14 +762,14 @@ TEST(Call, Basic_API) {
call_receive_inbound_message(call3, commit2).value();
call_apply_block(call3, block31).value();
auto commit3 = call_pull_outbound_messages(call3).value().at(0);
auto commit3 = F(call_pull_outbound_messages(call3).value().at(0)).value();
CHECK(commit2 != commit3);
call_receive_inbound_message(call2, commit3).value();
call_receive_inbound_message(call3, commit3).value();
auto reveal2 = call_pull_outbound_messages(call2).value().at(0);
auto reveal3 = call_pull_outbound_messages(call3).value().at(0);
auto reveal2 = F(call_pull_outbound_messages(call2).value().at(0)).value();
auto reveal3 = F(call_pull_outbound_messages(call3).value().at(0)).value();
call_receive_inbound_message(call2, reveal2).value();
call_receive_inbound_message(call2, reveal3).value();
call_receive_inbound_message(call3, reveal2).value();
@ -736,21 +778,26 @@ TEST(Call, Basic_API) {
ASSERT_EQ(call_get_verification_state(call2).value().emoji_hash.value(),
call_get_verification_state(call3).value().emoji_hash.value());
auto e = call_encrypt(call2, "hello").value();
auto e2 = call_encrypt(call2, "hello").value();
auto e = call_encrypt(call2, 1, "hello").value();
auto e2 = call_encrypt(call2, 1, "hello").value();
CHECK(e != "hello");
LOG(ERROR) << e.size();
ASSERT_EQ("hello", call_decrypt(call2, e).value());
// ASSERT_TRUE(!call_decrypt(call2, e).is_ok()); // uncomment when replay protection is written
ASSERT_EQ("hello", call_decrypt(call3, e).value());
ASSERT_TRUE(!call_decrypt(call2, 2, 1, e).is_ok());
// ASSERT_TRUE(!call_decrypt(call3, 2, 2, e).is_ok()); Uncomment if we will validate channel_id
ASSERT_TRUE(!call_decrypt(call3, 1, 1, e).is_ok());
ASSERT_EQ("hello", call_decrypt(call3, 2, 1, e).value());
ASSERT_TRUE(!call_decrypt(call3, 2, 1, e).is_ok());
auto block3 =
call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}})
F(call_create_change_state_block(call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
.value();
call_apply_block(call3, block3).value();
ASSERT_TRUE(!call_decrypt(call3, e).is_ok());
ASSERT_EQ("hello", call_decrypt(call3, e2).value());
ASSERT_TRUE(!call_decrypt(call2, call_encrypt(call3, "bye").value()).is_ok());
ASSERT_TRUE(!call_decrypt(call3, 2, 1, e).is_ok());
ASSERT_EQ("hello", call_decrypt(call3, 2, 1, e2).value());
ASSERT_TRUE(call_decrypt(call2, 3, 1, call_encrypt(call3, 1, "bye").value()).is_ok());
LOG(ERROR) << call_describe(call1).value();
LOG(ERROR) << call_describe(call2).value();
key_destroy_all();
call_destroy_all();

View File

@ -0,0 +1,51 @@
#include "EncryptionTestVectors.h"
#include "td/e2e/MessageEncryption.h"
#include "td/utils/misc.h"
#include "td/utils/simple_tests.h"
#include "td/utils/tests.h"
namespace tde2e_core {
class EncryptionTest {
public:
static td::SecureString encrypt_data_with_deterministic_padding(td::Slice data, td::Slice secret) {
auto prefix = MessageEncryption::gen_deterministic_prefix(data.size(), 16);
td::SecureString combined(prefix.size() + data.size());
combined.as_mutable_slice().copy_from(prefix);
combined.as_mutable_slice().substr(prefix.size()).copy_from(data);
return MessageEncryption::encrypt_data_with_prefix(combined.as_slice(), secret);
}
};
}
using namespace tde2e_core;
S_TEST(EncryptionTest, test_vectors) {
auto test_vectors = get_test_vectors();
for (const auto &vec : test_vectors) {
LOG(INFO) << "Testing vector: " << vec.name;
// Convert hex strings to binary
auto secret = td::hex_decode(vec.secret).move_as_ok();
auto data = td::hex_decode(vec.data).move_as_ok();
auto header = td::hex_decode(vec.header).move_as_ok();
auto expected_encrypted = td::hex_decode(vec.encrypted).move_as_ok();
auto expected_encrypted_header = td::hex_decode(vec.encrypted_header).move_as_ok();
// Test encrypt_data with deterministic padding
auto encrypted = EncryptionTest::encrypt_data_with_deterministic_padding(data, secret);
// Test encrypt_header
auto encrypted_header_result = MessageEncryption::encrypt_header(header, encrypted, secret);
ASSERT_TRUE(encrypted_header_result.is_ok());
// For simplicity during debugging, verify only decryption
auto decrypted_result = MessageEncryption::decrypt_data(expected_encrypted, secret);
ASSERT_TRUE(decrypted_result.is_ok());
ASSERT_EQ(td::hex_encode(decrypted_result.ok()), vec.data);
auto decrypted_header_result = MessageEncryption::decrypt_header(expected_encrypted_header, expected_encrypted, secret);
ASSERT_TRUE(decrypted_header_result.is_ok());
ASSERT_EQ(td::hex_encode(decrypted_header_result.ok()), vec.header);
}
return td::Status::OK();
}

View File

@ -752,6 +752,21 @@ static void init_thread_local_evp_md_ctx(const EVP_MD_CTX *&evp_md_ctx, const ch
evp_md_ctx = nullptr;
}));
}
static void init_thread_local_evp_mac_ctx(EVP_MAC_CTX *&evp_mac_ctx, const char *digest) {
EVP_MAC *hmac = EVP_MAC_fetch(nullptr, "HMAC", nullptr);
LOG_IF(FATAL, hmac == nullptr);
evp_mac_ctx = EVP_MAC_CTX_new(hmac);
LOG_IF(FATAL, evp_mac_ctx == nullptr);
OSSL_PARAM params[2];
params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, const_cast<char *>(digest), 0);
params[1] = OSSL_PARAM_construct_end();
EVP_MAC_CTX_set_params(evp_mac_ctx, params);
EVP_MAC_free(hmac);
detail::add_thread_local_destructor(create_destructor([&evp_mac_ctx]() mutable {
EVP_MAC_CTX_free(const_cast<EVP_MAC_CTX *>(evp_mac_ctx));
evp_mac_ctx = nullptr;
}));
}
#endif
void sha1(Slice data, unsigned char output[20]) {
@ -967,26 +982,27 @@ void pbkdf2_sha512(Slice password, Slice salt, int iteration_count, MutableSlice
}
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
static void hmac_impl(const char *digest, Slice key, Slice message, MutableSlice dest) {
EVP_MAC *hmac = EVP_MAC_fetch(nullptr, "HMAC", nullptr);
LOG_IF(FATAL, hmac == nullptr);
EVP_MAC_CTX *ctx = EVP_MAC_CTX_new(hmac);
LOG_IF(FATAL, ctx == nullptr);
OSSL_PARAM params[2];
params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, const_cast<char *>(digest), 0);
params[1] = OSSL_PARAM_construct_end();
int res = EVP_MAC_init(ctx, const_cast<unsigned char *>(key.ubegin()), key.size(), params);
static void hmac_impl_finish(EVP_MAC_CTX *ctx, Slice key, Slice message, MutableSlice dest) {
int res = EVP_MAC_init(ctx, const_cast<unsigned char *>(key.ubegin()), key.size(), nullptr);
LOG_IF(FATAL, res != 1);
res = EVP_MAC_update(ctx, message.ubegin(), message.size());
LOG_IF(FATAL, res != 1);
res = EVP_MAC_final(ctx, dest.ubegin(), nullptr, dest.size());
LOG_IF(FATAL, res != 1);
EVP_MAC_CTX_free(ctx);
EVP_MAC_free(hmac);
}
static void hmac_impl_sha256(Slice key, Slice message, MutableSlice dest) {
static TD_THREAD_LOCAL EVP_MAC_CTX *ctx = nullptr;
if (ctx == nullptr) {
init_thread_local_evp_mac_ctx(ctx, "SHA256");
}
hmac_impl_finish(ctx, key, message, dest);
}
static void hmac_impl_sha512(Slice key, Slice message, MutableSlice dest) {
static TD_THREAD_LOCAL EVP_MAC_CTX *ctx = nullptr;
if (ctx == nullptr) {
init_thread_local_evp_mac_ctx(ctx, "SHA512");
}
hmac_impl_finish(ctx, key, message, dest);
}
#else
static void hmac_impl(const EVP_MD *evp_md, Slice key, Slice message, MutableSlice dest) {
@ -1001,7 +1017,7 @@ static void hmac_impl(const EVP_MD *evp_md, Slice key, Slice message, MutableSli
void hmac_sha256(Slice key, Slice message, MutableSlice dest) {
CHECK(dest.size() == 256 / 8);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
hmac_impl("SHA256", key, message, dest);
hmac_impl_sha256(key, message, dest);
#else
hmac_impl(EVP_sha256(), key, message, dest);
#endif
@ -1010,7 +1026,7 @@ void hmac_sha256(Slice key, Slice message, MutableSlice dest) {
void hmac_sha512(Slice key, Slice message, MutableSlice dest) {
CHECK(dest.size() == 512 / 8);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
hmac_impl("SHA512", key, message, dest);
hmac_impl_sha512(key, message, dest);
#else
hmac_impl(EVP_sha512(), key, message, dest);
#endif

View File

@ -95,7 +95,7 @@ class StatusTest : public Test {
LOG(INFO) << "Test " << get_test_name() << " PASSED";
} else {
// Include debug context in error message if available
LOG(ERROR) << "Test " << get_test_name() << " FAILED: " << status.message()
LOG(FATAL) << "Test " << get_test_name() << " FAILED: " << status.message()
<< simple_test::DebugContext::instance();
}
}