Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 71 additions & 21 deletions Dawa.sln
Original file line number Diff line number Diff line change
@@ -1,32 +1,82 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dawa", "src\Dawa\Dawa.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dawa.Console", "samples\Dawa.Console\Dawa.Console.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dawa.Demo", "samples\Dawa.Demo\Dawa.Demo.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
EndProject

Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dawa.Tests", "tests\Dawa.Tests\Dawa.Tests.csproj", "{E6966028-2FC7-4734-9FDA-77BD0F825B7F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.ActiveCfg = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.Build.0 = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.ActiveCfg = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.Build.0 = Debug|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.ActiveCfg = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.Build.0 = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.ActiveCfg = Release|Any CPU
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.Build.0 = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|x64.ActiveCfg = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|x64.Build.0 = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|x86.ActiveCfg = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Debug|x86.Build.0 = Debug|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|Any CPU.Build.0 = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|x64.ActiveCfg = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|x64.Build.0 = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|x86.ActiveCfg = Release|Any CPU
{E6966028-2FC7-4734-9FDA-77BD0F825B7F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E6966028-2FC7-4734-9FDA-77BD0F825B7F} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
15 changes: 13 additions & 2 deletions src/Dawa/Auth/AuthState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ public sealed class AuthState
[JsonPropertyName("lastAccountSyncTimestamp")]
public long LastAccountSyncTimestamp { get; set; }

/// <summary>
/// ADVSignedDeviceIdentity protobuf bytes (set during QR pairing).
/// Used for device-identity node when sending pkmsg.
/// </summary>
[JsonPropertyName("account")]
public byte[]? Account { get; set; }

/// <summary>True if this is a fresh (unauthenticated) state.</summary>
[JsonIgnore]
public bool IsFresh => Me == null;
Expand Down Expand Up @@ -96,9 +103,13 @@ public static AuthState CreateNew()
private static byte[] SignPreKey(byte[] identityPriv, byte[] preKeyPub)
{
// XEdDSA: uses the Curve25519 identity private key directly as an Ed25519 scalar.
// This matches Baileys / Signal's curve25519-js curve25519_sign() behaviour.
// WhatsApp requires signing the 33-byte KEY_BUNDLE_TYPE form: [0x05 || pubKey].
// Baileys signedPreKey: curve.sign(identityKey.private, Buffer.concat([Buffer.from([5]), signedPreKey.public]))
// Returns a 64-byte signature (R ‖ s).
return XEdDSA.Sign(identityPriv, preKeyPub);
var message = new byte[33];
message[0] = 0x05; // KEY_BUNDLE_TYPE
preKeyPub.CopyTo(message, 1);
return XEdDSA.Sign(identityPriv, message);
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/Dawa/Auth/SessionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ public async Task<AuthState> LoadAsync(CancellationToken ct = default)
var state = JsonSerializer.Deserialize<AuthState>(json, _jsonOpts);
if (state != null) return state;
}
return AuthState.CreateNew();
// No saved session — generate fresh keys and save immediately.
// This ensures reconnects during QR phase (before pairing) reuse the same keys
// that were encoded in the QR, so the phone can pair successfully.
var fresh = AuthState.CreateNew();
await SaveAsync(fresh, ct);
return fresh;
}

/// <summary>Saves the current auth state to disk.</summary>
Expand Down
31 changes: 31 additions & 0 deletions src/Dawa/Binary/BinaryNodeDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,22 @@ private static byte[] ReadBinary(ref BinaryReader reader)
return reader.ReadBytes(length);
}

// Sentinel returned when ReadString encounters StreamEnd (byte 0x02).
internal const string StreamEndSentinel = "\x02";

private static string ReadString(ref BinaryReader reader)
{
var b = reader.ReadByte();

// StreamEnd byte — signals server closed the stream.
if (b == WATags.StreamEnd)
return StreamEndSentinel;

// Single-byte tokens: byte value IS the index into SingleByteTokens (Baileys convention).
if (b >= 1 && b < WATags.DictionaryBase)
return WATags.GetSingleByteToken(b) ?? $"[TOKEN{b}]";

// Double-byte tokens: next byte is the index into the chosen dictionary.
if (b >= WATags.DictionaryBase && b <= WATags.DictionaryBase + 3)
{
int dictIndex = b - WATags.DictionaryBase;
Expand Down Expand Up @@ -123,6 +135,25 @@ private static string ReadString(ref BinaryReader reader)
var server = ReadString(ref reader);
return $"{user}@{server}";
}
case WATags.AdJid:
{
// AD_JID (0xF7): domainType byte + device byte + user string
// Baileys: readAdJid() in decode.js
var domainType = reader.ReadByte();
var device = reader.ReadByte();
var user = ReadString(ref reader);
var server = (domainType == 0 || domainType == 128) ? "s.whatsapp.net" : "lid";
return device == 0 ? $"{user}@{server}" : $"{user}:{device}@{server}";
}
case WATags.InteropJid:
case WATags.FbJid:
{
// Consume bytes to stay in sync, return placeholder
var domainType = reader.ReadByte();
var device = reader.ReadByte();
var user = ReadString(ref reader);
return $"{user}@fb";
}
case WATags.Nibble8:
{
var size = reader.ReadByte();
Expand Down
30 changes: 26 additions & 4 deletions src/Dawa/Binary/BinaryNodeEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,42 @@ private static void WriteString(Stream s, string value)
return;
}

// Try dictionary lookup first
if (WATags.TryGetToken(value, out var dictByte, out var idxByte))
// Try single-byte token first (1 byte)
if (WATags.TryGetSingleByteToken(value, out var singleByte))
{
s.WriteByte(singleByte);
return;
}

// Try double-byte token (2 bytes: DictionaryBase+dict, index)
if (WATags.TryGetDoubleByteToken(value, out var dictByte, out var idxByte))
{
s.WriteByte(dictByte);
s.WriteByte(idxByte);
return;
}

// Check if it's a JID (user@server)
// Check if it's a JID (user@server or @server with empty user)
var atIdx = value.IndexOf('@');
if (atIdx > 0)
if (atIdx >= 0)
{
var user = value[..atIdx];
var server = value[(atIdx + 1)..];

// Check if this is an AD-JID (multi-device) with format "user:device@server"
var colonIdx = user.IndexOf(':');
if (colonIdx >= 0 && int.TryParse(user[(colonIdx + 1)..], out var deviceId))
{
var baseUser = user[..colonIdx];
// AD_JID format: tag(247) + domainType + device + writeString(user)
byte domainType = server == "lid" ? (byte)1 : (byte)0;
s.WriteByte(WATags.AdJid);
s.WriteByte(domainType);
s.WriteByte((byte)deviceId);
WriteString(s, baseUser);
return;
}

s.WriteByte(WATags.JidPair);
WriteString(s, user);
WriteString(s, server);
Expand Down
Loading
Loading