37

Playing with the DDI Character Builder API

24 January 2011 in Articles by

For anyone who’s been playing with the two new web-services that the online version of WotC’s Character Builder uses, you may have tried remotely logging in, but wondered how the password needs to be encrypted?

Well I’ve cracked the way passwords are encrypted and have been able to login remotely and do simple actions such as, get a list of all my characters and export the XML for an old style DnD4e file.

Here’s a simple bit of C# showing how to encode a password.

public static byte[] SimpleEncrypt(string value, string key)
{
    byte[] buffer2;
    ICryptoTransform transform = GetSimpleAlgorithm(key).CreateEncryptor();
    using (MemoryStream stream = new MemoryStream())
    {
        using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write))
        {
            byte[] bytes = Encoding.UTF8.GetBytes(value);
            stream2.Write(bytes, 0, bytes.Length);
            stream2.Flush();
            stream2.FlushFinalBlock();
            stream.Position = 0L;
            buffer2 = stream.ToArray();
        }
    }
    return buffer2;
}

private static SymmetricAlgorithm GetSimpleAlgorithm(string key)
{
    AesManaged aes = new AesManaged();
    byte[] source = new SHA256Managed().ComputeHash(Encoding.UTF8.GetBytes(key));
    return new AesManaged { Key = source, IV = source.Take<byte>((aes.BlockSize / 8)).ToArray<byte>() };
}

As you can see this takes the username and password as inputs, if you’ve built a nice client wrapper around the SOAP webservice, something that Visual Studio does for me, then logging in is as simple as calling the login method on either of the web services.

contentClient.Login(username, SimpleEncrypt(password, username));

If we then use the GetAvailableContent() method of the ContentVault service, we can return and enumerate a list of characters.

ContentInfo[] content = contentClient.GetAvailableContent(0);

for (int i = 0; i < content.Length; i++)
{
    XDocument doc = XDocument.Parse(content[i].CommittedContent.Details.ToString());
    var name = doc.Element("CharacterDetails").Element("Name").Value;
    Console.WriteLine("{0} : {1}", i, name);
}

Then using the GetData() method of the same service we can grab the DnD4e XML file.

DataWithVersion data = contentClient.GetData(
    new ContentVault.ContentIdentifier { ContentID = content[0].CommittedContent.Identifier.ContentID },
    null
    );

RawContentBlob blob = data.Data as RawContentBlob;

String charFile = new UTF8Encoding().GetString(blob.RawData, 0, blob.RawData.Length);

File.WriteAllBytes("d:\\temp.dnd4e", blob.RawData);

There you go just a brief window into some of the things the ContentVault service can do.

No word from WotC on whether or not we’re allowed to do this though. I did ask, but never got a response.

One last note, if you’re doing this in Visual Studio then make sure the bindings for the web services have allowCookies set to true, then the login will be remembered for the subsequent calls to the service.

If you’re consuming the SOAP in some other not Microsoft way, then obviously you’ll need to do the same thing, save the cookie from the login response and send it on with any following requests.

Since the dark days of the late '70s, Iain has been playing and running RPGs of one type or another. When not with his wife and children or working as a software engineer/Photoshop monkey he spends spare time either shooting pointy sticks at pointless paper targets, practising the art of card magic, or spending time enjoying the varied worlds of role playing.

Tags: , , , ,

37 Comments to Playing with the DDI Character Builder API

  1. [...] This post was mentioned on Twitter by RPG Bloggers Network, Iain M Norman. Iain M Norman said: More info on using the Character Builder API / Web services. http://is.gd/7qtTEm #DnD #DDI [...]

  2. Tweets that mention DDI Character Builder API hacking | Buccaneers Guild -- Topsy.com on 24 January 2011
  3. Ha! There seems to be someone roughly familiar with all those compendium stuff third party implementation thing! As I see some mobile apps and things going on I wondered how the legal situation is whith those APIs. I mean, clearly some of the apps use the compendium API. You mentioned that WotC does not clarify how the lega situation is for the new APIs. But do you know how the situation is for the “old” compendium API and other API that are available? I never found any information regarding that. If one could use them that could open up whole new possibilities.

  4. TheClone on 26 January 2011
  5. The Compendium API once had instructions on how to use it on the wizards.com site, so I think that’s fairly safe to use. It is not zehr gut though, it’s just a search API, you can’t actually get API access to the compendium entries, just search results. The search results include the URI of the compendium entry, which you could programatically access with a DDI login, or ask for the users login. Both have issues. Masterplan did some scraping of the entries like this, and got into trouble for storing them. It’s okay it seems to get them when you need them, just not rebuild the database at your end.

    Masterplan still exist and haven’t been sued into oblivion, so I’d say in summing up, try anything you like until you’re told to stop. Seems to be the only way to get them talking to you maybe?

  6. Iain M Norman on 26 January 2011
  7. Yeah looks like. Would be nice to have some openness and they frankly tell you what they want you to do and what not. Thanks for the info. Now I have at least some point to start from.

  8. TheClone on 28 January 2011
  9. This is fantastic! Do you happen to have this code in VB?

  10. MrNiceGuy on 9 May 2011
  11. I haven’t got it in VB no. Are you okay enough with C# syntax to convert it?

  12. Iain M Norman on 9 May 2011
  13. I’ve tried converting it from C# – but, it can’t negotiate the encryption correctly. If anyone has a VB version, it would be greatly appreciated :)

  14. MrNiceGuy on 25 May 2011
  15. Public Shared Function SimpleEncrypt(value As String, key As String) As Byte()
    Dim buffer2 As Byte()
    Dim transform As ICryptoTransform = GetSimpleAlgorithm(key).CreateEncryptor()
    Using stream As New MemoryStream()
    Using stream2 As New CryptoStream(stream, transform, CryptoStreamMode.Write)
    Dim bytes As Byte() = Encoding.UTF8.GetBytes(value)
    stream2.Write(bytes, 0, bytes.Length)
    stream2.Flush()
    stream2.FlushFinalBlock()
    stream.Position = 0L
    buffer2 = stream.ToArray()
    End Using
    End Using
    Return buffer2
    End Function

    Private Shared Function GetSimpleAlgorithm(key As String) As SymmetricAlgorithm
    Dim aes As New AesManaged()
    Dim source As Byte() = New SHA256Managed().ComputeHash(Encoding.UTF8.GetBytes(key))
    Return New AesManaged() With { _
    Key .Key = source, _
    Key .IV = source.Take(Of Byte)((aes.BlockSize / 8)).ToArray(Of Byte)() _
    }
    End Function

  16. Iain M Norman on 25 May 2011
  17. Hopefully that helps. I haven’t tested though.

  18. Iain M Norman on 25 May 2011
  19. Thanks! I’ll give it a whirl tonight!

  20. MrNiceGuy on 27 May 2011
  21. Ok… I made some changes (since it wasn’t quite VB friendly)… but still can’t get it to work. Here’s the code (it compiles) – but won’t return a valid login:

    Public Shared Function SimpleEncrypt(ByVal value As String, ByVal key As String) As Byte()

    Dim buffer2 As Byte()
    Dim Transform As ICryptoTransform

    Transform = GetSimpleAlgorithm(key).CreateEncryptor
    Dim stream As New MemoryStream()
    Dim stream2 As New CryptoStream(stream, Transform, CryptoStreamMode.Write)
    Dim bytes As Byte()

    Using stream
    bytes = Encoding.UTF8.GetBytes(value)
    stream2.Write(bytes, 0, bytes.Length)
    stream2.Flush()
    stream2.FlushFinalBlock()
    stream.Position = 0L
    buffer2 = stream.ToArray()

    End Using

    Return buffer2

    End Function

    Private Shared Function GetSimpleAlgorithm(ByVal key As String) As SymmetricAlgorithm

    Dim aes As New AesManaged()
    Dim sha As SHA256Managed = New SHA256Managed()

    Dim source As Byte() = sha.ComputeHash(Encoding.UTF8.GetBytes(key))
    Dim myMgr As New AesManaged
    myMgr.IV = source.Take(aes.BlockSize / 8).ToArray()
    myMgr.Key = source

    Return myMgr

    End Function

    Any ideas?

  22. MrNiceGuy on 27 May 2011
  23. Mail me a zip of your solution and I’ll take a look.

  24. Iain M Norman on 27 May 2011
  25. Thanks Iain! It works like a charm! I’m making a Windows 8 character viewer. I’ll try to send you the end result ;) .

  26. tec-goblin on 13 January 2012
  27. A metro style app?

  28. Iain M Norman on 13 January 2012
  29. Yes, though I converted the code to WinRT and have a COMException on the Encrypt line. It seems to me like a bug (I sent it to Microsoft):
    private static byte[] SimpleEncrypt(string value, string key)
    {
    var simpleAlgorithm = GetSimpleAlgorithm(key);
    var encryptedBuffer = CryptographicEngine.Encrypt(simpleAlgorithm.Item1,
    CryptographicBuffer.ConvertStringToBinary(value, BinaryStringEncoding.Utf8),
    simpleAlgorithm.Item2);
    //bug here with CBC
    var result = new byte[encryptedBuffer.Length];
    encryptedBuffer.CopyTo(result);
    return result;
    }

    private static Tuple GetSimpleAlgorithm(string key)
    {
    var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(“AES_CBC”);
    var keyAsBinary = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8);
    var source = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256).HashData(keyAsBinary);
    var shortKey = CryptographicBuffer.CreateFromByteArray(UTF8Encoding.UTF8.GetBytes(key).Take((int)provider.BlockLength).ToArray());

    return new Tuple(provider.CreateSymmetricKey(source), shortKey);
    }

  30. tec-goblin on 17 February 2012
  31. What are the web service urls

  32. midavis on 5 April 2012
  33. I don’t remember off hand. A quick check with fiddler or something will tell you.

  34. Iain M Norman on 7 April 2012
  35. By the way, in the aforementioned Windows 8 code I think the correct algorithm is AES_CBC_PKCS7.

  36. tec-goblin on 7 April 2012
  37. Thanks for the clarification.

  38. Iain M Norman on 7 April 2012
  39. I’ve also been working on porting this to a Metro app, and I’ve managed to fix the problems with tec-goblin’s code. The main problem was the fact that he was using the “key” string to create the IV, whereas he should have used the hash created from the string, instead.

    private static byte[] SimpleEncrypt(string value, string key)
    {
    var simpleAlgorithm = GetSimpleAlgorithm(key);
    CryptographicKey encryptKey = simpleAlgorithm.Item1;
    IBuffer IV = simpleAlgorithm.Item2;
    var encryptedBuffer = CryptographicEngine.Encrypt(encryptKey, CryptographicBuffer.ConvertStringToBinary(value, BinaryStringEncoding.Utf8), IV);

    var result = new byte[encryptedBuffer.Length];
    CryptographicBuffer.CopyToByteArray(encryptedBuffer, out result);
    return result;
    }

    private static Tuple GetSimpleAlgorithm(string key)
    {
    var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesCbcPkcs7);
    var keyAsBinary = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8);
    var source = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256).HashData(keyAsBinary);

    byte[] sourceArray = new byte[16];
    CryptographicBuffer.CopyToByteArray(source, out sourceArray);

    var shortKey = CryptographicBuffer.CreateFromByteArray(sourceArray.Take((int)provider.BlockLength).ToArray());

    return new Tuple(provider.CreateSymmetricKey(source), shortKey);
    }

  40. James W on 6 November 2012
  41. Many thanks for sharing that with everyone James.

  42. Iain M Norman on 6 November 2012
  43. Thank YOU for sharing this in the first place.

    There were some problems with the GetSimpleAlgorithm code that got posted on the comment; problems with the less-than and greater-than signs. Let’s try that again, shall we?

    private static Tuple<CryptographicKey, IBuffer> GetSimpleAlgorithm(string key)
    {
    var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesCbcPkcs7);
    var keyAsBinary = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8);
    var source = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256).HashData(keyAsBinary);

    byte[] sourceArray = new byte[16];
    CryptographicBuffer.CopyToByteArray(source, out sourceArray);

    var shortKey = CryptographicBuffer.CreateFromByteArray(sourceArray.Take((int)provider.BlockLength).ToArray());

    return new Tuple<CryptographicKey,IBuffer>(provider.CreateSymmetricKey(source), shortKey);
    }

  44. James W on 6 November 2012
  45. Now, if only you had that available in Visual Basic format… ;)

  46. Eric H on 6 November 2012
  47. Any good?

    Private Shared Function GetSimpleAlgorithm(key As String) As Tuple(Of CryptographicKey, IBuffer)
    Dim provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesCbcPkcs7)
    Dim keyAsBinary = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8)
    Dim source = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256).HashData(keyAsBinary)

    Dim sourceArray As Byte() = New Byte(15) {}
    CryptographicBuffer.CopyToByteArray(source, sourceArray)

    Dim shortKey = CryptographicBuffer.CreateFromByteArray(sourceArray.Take(CInt(provider.BlockLength)).ToArray())

    Return New Tuple(Of CryptographicKey, IBuffer)(provider.CreateSymmetricKey(source), shortKey)
    End Function

  48. Iain M Norman on 6 November 2012
  49. Thanks James W!! I am not very strong in cryptography (because I use it directly so rarely and I forget the basics). I will soon start working again on my application and your fix will be invaluable!

  50. tec-goblin on 6 November 2012
  51. Hmmm – code snippet not good (trying to use it in a web app build in .NET). Some quick research seems to indicate some of the method calls are only available in Windows 8?

  52. Eric H on 10 November 2012
  53. Well, yeah, the code tec-goblin originally posted and I got working was specifically written for Windows 8 Metro apps, because the classes the original .NET code used weren’t available for Metro apps. Since you replied right after I had posted the Metro version (after months of no other activity), people kinda assumed you were referring to the new stuff.

    If you’re looking to build a .NET app, you’ll want to use the code in the original post as your starting point; not sure how the “web app” part will affect whether or not it’ll work, but it’s your best bet.

    Mr. Nice Guy was apparently ALSO working on a VB version, but he said the last version he posted here wasn’t returning a valid login.

  54. James W on 12 November 2012
  55. Yeah,… me = Mr. Nice Guy :) I’ve been trying to get this stuff working for years now, but continue to fail to return a valid login.

  56. Eric H on 12 November 2012
  57. The code was ripped out of WotC’s silverlight app so I can’t off hand think of anything that wouldn’t work in an aspx page.

    Eric is it asp.net you’re attempting to get it working in?

  58. Iain M Norman on 12 November 2012
  59. Here’s the barest interaction in ASP.net, showing logging in and retrieval of a list of characters.

    http://buccaneersguild.com/wp-content/uploads/2012/11/Web.zip

  60. Iain M Norman on 12 November 2012
  61. It’s a straight asp.net application (but vb-flavored)… using VS2008.

    I’ve tried the code you sent (after converting to vb) and now I’m getting this error (upon trying to execute the login statement):
    SOAP header Action was not understood.

    Here’s the code:

    Imports System.Security.Cryptography
    Imports System.Security.Cryptography.HashAlgorithm
    Imports System.IO
    Imports System.Text

    Imports com.wizards.ContentService
    Imports System
    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Web
    Imports System.Web.UI
    Imports System.Web.UI.WebControls
    Imports System.Xml.Linq

    Partial Class _Default
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    Try
    Dim username As String = “username”
    Dim password As String = “password”

    Dim contentClient As New com.wizards.ContentService.ContentVaultService
    Dim workspaceClient As New com.wizards.vecna.D20WorkspaceService

    Dim loggedIn As Boolean = False
    Dim loggedInResult As Boolean = False

    Call contentClient.Login(username, SimpleEncrypt(password, username), loggedIn, loggedInResult)

    If loggedIn Then
    Dim content As ContentInfo() = contentClient.GetAvailableContent(0, True)
    Dim doc As XDocument
    Dim name As String

    For intX As Integer = 0 To content.Length
    doc = XDocument.Parse(content(intX).CommittedContent.Details.ToString)
    name = doc.Element(“CharacterDetails”).Element(“Name”).Value
    Response.Write(String.Format(“{0} : {1}”, intX, name))
    Next

    End If

    Catch ex As Exception

    End Try

    End Sub

    Public Shared Function SimpleEncrypt(ByVal value As String, ByVal key As String) As Byte()

    Dim buffer2 As Byte()
    Dim Transform As ICryptoTransform

    Transform = GetSimpleAlgorithm(key).CreateEncryptor
    Dim stream As New MemoryStream()
    Dim stream2 As New CryptoStream(stream, Transform, CryptoStreamMode.Write)
    Dim bytes As Byte()

    Using stream
    bytes = Encoding.UTF8.GetBytes(value)
    stream2.Write(bytes, 0, bytes.Length)
    stream2.Flush()
    stream2.FlushFinalBlock()
    stream.Position = 0L
    buffer2 = stream.ToArray()

    End Using

    Return buffer2

    End Function

    Private Shared Function GetSimpleAlgorithm(ByVal key As String) As SymmetricAlgorithm

    Dim aes As New AesManaged()
    Dim sha As SHA256Managed = New SHA256Managed()

    Dim source As Byte() = sha.ComputeHash(Encoding.UTF8.GetBytes(key))
    Dim myMgr As New AesManaged
    myMgr.IV = source.Take(aes.BlockSize / 8).ToArray()
    myMgr.Key = source

    Return myMgr

    End Function

    End Class

  62. Eric H on 12 November 2012
  63. Ok – I imported the WSDL files from the project you uploaded and that made a huge difference! I’m now able to achieve a valid login, however, as soon as it tries to pull in data (at this line)
    ContentInfo[] content = contentClient.GetAvailableContent(0);

    I then get an error “Your session has expired”

    This one has me quite stumped… but – at least I’m getting closer!

  64. Eric H on 12 November 2012
  65. Eric H, try setting AllowCookies = true! (I hope you have figured it by now, it’s just that I read the forum again, trying to set up the proper binding – in Metro we have to do it by hand).

  66. tec-goblin on 25 March 2013
  67. Good spot, yes that does look like a cookies related error. God knows what DNDNext is going to bring. I don’t hold out for a decent API.

  68. Iain M Norman on 25 March 2013
  69. Any C# D&D people fancy working on a project together?

  70. Iain M Norman on 30 March 2013
  71. I haven’t gotten it working yet – but, I’ll give the cookies a try… I wish this were a little easier :)

  72. Eric H on 1 April 2013
  73. Got it working yet Eric?

  74. Iain M Norman on 25 April 2013

Leave a comment