I'm implementing local encryption within an existing file management program.
Much of the example code I can find, such as Microsoft's, demonstrates how to write directly to a file, but what I need to do is provide a stream that is consumed elsewhere in the program:
CryptoStream GetEncryptStream(string filename)
{
var rjndl = new RijndaelManaged();
rjndl.KeySize = 256;
rjndl.BlockSize = 256;
rjndl.Mode = CipherMode.CBC;
rjndl.Padding = PaddingMode.PKCS7;
// Open read stream of unencrypted source fileStream:
var fileStream = new FileStream(filename, FileMode.Open);
/* Get key and iv */
var transform = rjndl.CreateEncryptor(key, iv);
// CryptoStream in *read* mode:
var cryptoStream = new CryptoStream(fileStream, transform, CryptoStreamMode.Read);
/* What can I do here to insert the unencrypted IV at the start of the
stream so that the first X bytes returned by cryptoStream.Read are
the IV, before the bytes of the encrypted file are returned? */
return cryptoStream; // Return CryptoStream to be consumed elsewhere
}
My issue is outlined in the comment on the last line but one: how can I add the IV to the start of the CryptoStream such that it will be the first X bytes returned when the CryptoStream is read, given that control of when to actually start reading the stream and writing to a file is outside the scope of my code?
Ok... now that your problem is clear, it is "quite" easy... Sadly .NET doesn't include a class to merge two Stream, but we can easily create it. The MergedStream is a read-only, forward-only multi-Stream merger.
You use like:
var mergedStream = new MergedStream(new Stream[]
{
new MemoryStream(iv),
cryptoStream,
});
Now... When someone tries to read from the MergedStream, first the MemoryStream containing the IV will be consumed, then the cryptoStream will be consumed.
public class MergedStream : Stream
{
private Stream[] streams;
private int position = 0;
private int currentStream = 0;
public MergedStream(Stream[] streams) => this.streams = streams;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => streams.Sum(s => s.Length);
public override long Position
{
get => position;
set => throw new NotSupportedException();
}
public override void Flush()
{
}
public override int Read(byte[] buffer, int offset, int count)
{
if (streams == null)
{
throw new ObjectDisposedException(nameof(MergedStream));
}
if (currentStream >= streams.Length)
{
return 0;
}
int read;
while (true)
{
read = streams[currentStream].Read(buffer, offset, count);
position += read;
if (read != 0)
{
break;
}
currentStream++;
if (currentStream == streams.Length)
{
break;
}
}
return read;
}
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
try
{
if (disposing && streams != null)
{
for (int i = 0; i < streams.Length; i++)
{
streams[i].Close();
}
}
}
finally
{
streams = null;
}
}
}
It is not a good design to use a CryptoStream to communicate between two local parties. You should use a generic InputStream or pipe (for inter-process communication) instead. Then you can combine a MemoryStream for the IV and a CryptoStream and return the combination. See the answer of xanatos on how to do this (you may still need to fill in the Seek functionality if that's required).
A CryptoStream will only ever be able to handle ciphertext. As you need to change the code at the receiver anyway if you'd want to decrypt you might as well refactor to InputStream.
If you're required to keep the current design then there is a hack available. First "decrypt" the IV using ECB mode without padding. As a single block cipher call always succeeds the result will be a block of data that - when encrypted using the CipherStream - turns into the IV again.
Steps:
generate 16 random bytes in an array, this will be the real IV;
decrypt the 16 byte IV using ECB without padding and the key used for the CipherStream;
initialize the CipherStream using the key and an all zero, 16 byte IV;
encrypt the "decrypted" IV using the CipherStream;
input the rest of the plaintext.
You will need to create an InputStream that first receives the decrypted IV (as MemoryStream) and then the plaintext (as FileStream) for this to be feasible. Again, also see the answer of xanatos on how to do this. Or see for instance this combiner and this HugeStream on good ol' StackOverflow. Then use the combined stream as source for the CipherInputStream.
But needless to say hacks like these should be well documented and removed at the earliest convenience.
Notes:
This trick won't work on any mode; it works for CBC mode, but other modes may use the IV differently;
Note that an OutputStream would generally make more sense for encryption, there may be other things wrong with the design.
Thanks for those who've taken the time to answer. In the end I realized I have to have knowledge of the IV length in the buffering code, there's no way around it, so elected to keep it simple:
Encryption method (Pseudo-code):
/* Get key and IV */
outFileStream.Write(IV); // Write IV to output stream
var transform = rijndaelManaged.CreateEncryptor(key, iv);
// CryptoStream in read mode:
var cryptoStream = new CryptoStream(inFileStream, transform, CryptoStreamMode.Read);
do
{
cryptoStream.Read(chunk, 0, blockSize); // Get and encrypt chunk
outFileStream.Write(chunk); // Write chunk
}
while (chunk.Length > 0)
/* Cleanup */
Decryption method (Pseudo-code):
/* Get key */
var iv = inFileStream.Read(ivLength); // Get IV from input stream
var transform = rijndaelManaged.CreateDecryptor(key, iv);
// CryptoStream in write mode:
var cryptoStream = new CryptoStream(outFileStream, transform, CryptoStreamMode.Write);
do
{
inFileStream.Read(chunk, 0, blockSize); // Get chunk
cryptoStream.Write(chunk); // Decrypt and write chunk
}
while (chunk.Length > 0)
/* Cleanup */
Related
The following is a simple compression method I wrote using DeflateStream:
public static int Compress(
byte[] inputData,
int inputStartIndex,
int inputLength,
byte[] outputData,
int outputStartIndex,
int outputLength)
{
if (inputData == null)
throw new ArgumentNullException("inputData must be non-null");
MemoryStream memStream = new MemoryStream(outputData, outputStartIndex, outputLength);
using (DeflateStream dstream = new DeflateStream(memStream, CompressionLevel.Optimal))
{
dstream.Write(inputData, inputStartIndex, inputLength);
return (int)(memStream.Position - outputStartIndex);
}
}
What is special in this method is that I didn't use the parameter-less constructor of MemoryStream. This is because it is a high-throughput server. Array outputData is rented from ArrayPool, to be used to hold the compressed bytes, so that after I make use of it I can return it to ArrayPool.
The compression happened properly, and the compressed data is properly placed into outputData, but memStream.Position was zero, so I can't find out how many bytes have been written into the MemoryStream.
Only part of outputData is occupied by the compressed data. How do I find out the length of the compressed data?
MemoryStream.Position is 0 because data was not actually written there yet at the point you read Position. Instead, tell DeflateStream to leave underlying stream (MemoryStream) open, then dispose DeflateStream. At this point you can be sure it's done writing whatever it needs. Now you can read MemoryStream.Position to check how many bytes were written:
public static int Compress(
byte[] inputData,
int inputStartIndex,
int inputLength,
byte[] outputData,
int outputStartIndex,
int outputLength)
{
if (inputData == null)
throw new ArgumentNullException("inputData must be non-null");
using (var memStream = new MemoryStream(outputData, outputStartIndex, outputLength)) {
// leave open
using (DeflateStream dstream = new DeflateStream(memStream, CompressionLevel.Optimal, leaveOpen: true)) {
dstream.Write(inputData, inputStartIndex, inputLength);
}
return (int) memStream.Position; // now it's not 0
}
}
You also don't need to substract outputStartIndex, because Position is already relative to that index you passed to constructor.
So I have a file upload form which (after uploading) encrypts the file and uploads it to an S3 bucket. However, I'm doing an extra step which I want to avoid. First, I'll show you some code what I am doing now:
using (MemoryStream memoryStream = new MemoryStream())
{
Security.EncryptFile(FileUpload.UploadedFile.OpenReadStream(), someByteArray, memoryStream);
memoryStream.Position = 0; // reset it's position
await S3Helper.Upload(objectName, memoryStream);
}
My Security.EncryptFile method:
public static void EncryptFile(Stream inputStream, byte[] key, Stream outputStream)
{
CryptoStream cryptoStream;
using (SymmetricAlgorithm cipher = Aes.Create())
using (inputStream)
{
cipher.Key = key;
// aes.IV will be automatically populated with a secure random value
byte[] iv = cipher.IV;
// Write a marker header so we can identify how to read this file in the future
outputStream.WriteByte(69);
outputStream.WriteByte(74);
outputStream.WriteByte(66);
outputStream.WriteByte(65);
outputStream.WriteByte(69);
outputStream.WriteByte(83);
outputStream.Write(iv, 0, iv.Length);
using (cryptoStream =
new CryptoStream(inputStream, cipher.CreateEncryptor(), CryptoStreamMode.Read))
{
cryptoStream.CopyTo(outputStream);
}
}
}
The S3Helper.Upload method:
public async static Task Upload(string objectName, Stream inputStream)
{
try
{
// Upload a file to bucket.
using (inputStream)
{
await minio.PutObjectAsync(S3BucketName, objectName, inputStream, inputStream.Length);
}
Console.Out.WriteLine("[Bucket] Successfully uploaded " + objectName);
}
catch (MinioException e)
{
Console.WriteLine("[Bucket] Upload exception: {0}", e.Message);
}
}
So, what happens above is I'm creating a MemoryStream, running the EncryptFile() method (which outputs it back to the stream), I reset the stream position and finally reuse it again to upload it to the S3 bucket (Upload()).
The question
What I'd like to do is the following (if possible): directly upload the uploaded file to the S3 bucket, without storing the full file in memory first (kinda like the code below, even though it's not working):
await S3Helper.Upload(objectName, Security.EncryptFile(FileUpload.UploadedFile.OpenReadStream(), someByteArray));
So I assume it has to return a buffer to the Upload method, which will upload it, and waits for the EncryptFile() method to return a buffer again until the file has been fully read. Any pointers to the right direction will be greatly appreciated.
What you could do is make your own EncryptionStream that overloads the Stream class. When you read from this stream, it will take a block from the inputstream, encrypt it and then output the encrypted data.
As an example, something like this:
public class EncrypStream : Stream {
private Stream _cryptoStream;
private SymmetricAlgorithm _cipher;
private Stream InputStream { get; }
private byte[] Key { get; }
public EncrypStream(Stream inputStream, byte[] key) {
this.InputStream = inputStream;
this.Key = key;
}
public override int Read(byte[] buffer, int offset, int count) {
if (this._cipher == null) {
_cipher = Aes.Create();
_cipher.Key = Key;
// aes.IV will be automatically populated with a secure random value
byte[] iv = _cipher.IV;
// Write a marker header so we can identify how to read this file in the future
// #TODO Make sure the BUFFER is big enough...
var idx = offset;
buffer[idx++] = 69;
buffer[idx++] = 74;
buffer[idx++] = 66;
buffer[idx++] = 65;
buffer[idx++] = 69;
buffer[idx++] = 83;
Array.Copy(iv, 0, buffer, idx, iv.Length);
offset = idx + iv.Length;
// Startup stream
this._cryptoStream = new CryptoStream(InputStream, _cipher.CreateEncryptor(), CryptoStreamMode.Read);
}
// Write block
return this._cryptoStream.Read(buffer, offset, count);
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
// Make SURE you properly dispose the underlying streams!
this.InputStream?.Dispose();
this._cipher?.Dispose();
this._cryptoStream?.Dispose();
}
// Omitted other methods from stream for readability...
}
Which allows you to call the stream as:
using (var stream = new EncrypStream(FileUpload.UploadedFile.OpenReadStream(), someByteArray)) {
await S3Helper.Upload(objectName, stream);
}
As I notice your upload method requires the total bytelength of the encrypted data, you can look into this post here to get an idea how you would be able to calculate this.
(I'm guessing that the CryptoStream does not return the expected length of the encrypted data, but please correct me if I'm wrong on this)
I would like to make some secure container for my application, and here's the map :
I finished opening/saving code now, and tested it, however, ArgumentException was thrown.
The code will run like this.
Create byte[] type variable for containing not crypted user data.
FileStream Writes Magic Number to first 5 bytes.
RijndaelManaged accepts key, and generates Initialization Vector.
FileStream Writes Initialization Vector to next 16 bytes. <- Exception thrown!
CryptoStream transform the variable from 1.
FileStream Writes the crypted data from 22th bytes.
Debugging, and I found the reason that FileStream.Read() has been thrown the Exception. and the message is:
Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.
I tried to set the length of stream as (user data) + 21. but it doesn't work. I attach entire code for saving file, and I hope this problem will be solved.
Thank you!
private bool SaveFile(string FilePath, bool IsCrypt)
{
byte[] Data = Encoding.UTF8.GetBytes(WorkspaceList[CurrentIndex]._textbox.Text);
using (var Stream = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
if (IsCrypt)
{
Stream.SetLength(Data.Length + 21); // Tried when I got Exception
Stream.Write(MagicNumber, 0, 5); //Magic Number
using (var CryptoHandler = new RijndaelManaged()) // AES256 Encryption
{
CryptoHandler.BlockSize = 128;
CryptoHandler.KeySize = 256;
CryptoHandler.Padding = PaddingMode.PKCS7;
CryptoHandler.Mode = CipherMode.CBC;
var tempKey = WorkspaceList[CurrentIndex]._cryptkey;
if(tempKey.Length < 32)
{
tempKey.PadRight(32);
}
else if (tempKey.Length > 32)
{
tempKey.Remove(33);
}
CryptoHandler.Key = Encoding.UTF8.GetBytes(WorkspaceList[CurrentIndex]._cryptkey.PadRight(32));
CryptoHandler.GenerateIV();
Stream.Write(CryptoHandler.IV, 5, 16); //IV Insertion *** ArgumentException ***
var CryptoInstance = CryptoHandler.CreateEncryptor(CryptoHandler.Key, CryptoHandler.IV);
using (var MemoryHandler = new MemoryStream())
{
using (var Crypto = new CryptoStream(MemoryHandler, CryptoInstance, CryptoStreamMode.Write))
{
byte[] _Buffer = Data;
Crypto.Read(Data, 0, Data.Length);
_Buffer = MemoryHandler.ToArray();
Stream.Write(_Buffer, 21, _Buffer.Length); // Insert Crypted Data
Stream.Close();
return true;
}
}
}
}
else
{
Stream.Write(Data, 0, Data.Length);
Stream.Close();
return true;
}
}
}
Replace Stream.Write(CryptoHandler.IV, 5, 16); //IV Insertion
With Stream.Write(CryptoHandler.IV, 0, CryptoHandler.IV.Length); //IV Insertion
array = CryptoHandler.IV (the data you want to write)
offset = 0 (you write from the first byte of array)
count = CryptoHandler.IV.Length (you write all bytes from CryptoHandler.IV)
Note that offset is intrinsic to array, not to the Stream. After a successful Write operation, the stream cursor waits at the last written position. I suppose you specified an offset of 5 to take into account the MagicNumber?
You would have add the same problem with Stream.Write(_Buffer, 21, _Buffer.Length);
I am trying to encrypt/decrypt bytes - I have done a lot of reading about the Key and IV for the AES algorithm using the AESManaged class in System.Security.Cryptography. I read James Johnson's answer to the following question http://www.techques.com/question/1-7025135/My-Length-of-the-data-to-decrypt-is-invalid-error where he suggests that you use a random IV in the encryption routine and prepend the IV to the encrypted message. The decrypt function strips off the random IV from the beginning of the encrypted message to initialize the decryption class and then decrypts the rest of the bytes. I have attempted to do this in the following code. But I keep getting the "Length of the data to decrypt is invalid." error message when I attempt the decrypt after the encryption. Could someone possibly shed some light on what might be wrong.
USAGE: (streamToEncrypt/streamToDecrypt are System.IO.Stream)
using (var cryptoHelper = new AESHelper())
{
var encryptedBytes = cryptoHelper.Encrypt(AESHelper.StreamToByteArray(streamToEncrypt));
}
using (var cryptoHelper = new AESHelper())
{
var decryptedBytes = cryptoHelper.Decrypt(AESHelper.StreamToByteArray(streamToDecrypt));
}
public class AESHelper : IDisposable
{
public AesManaged AESManaged;
internal ICryptoTransform Encryptor { get; set; }
internal ICryptoTransform Decryptor { get; set; }
private const string KEY = "2428GD19569F9B2C2341839416C8E87G";
private static readonly byte[] Salt = Encoding.ASCII.GetBytes("?pt1$8f]l4g80");
private const Int32 ITERATIONS = 1042;
internal AESHelper()
{
AESManaged = new AesManaged();
AESManaged.BlockSize = AESManaged.LegalBlockSizes[0].MaxSize;
AESManaged.KeySize = AESManaged.LegalKeySizes[0].MaxSize;
AESManaged.Mode= CipherMode.CBC;
}
public void KeyGenerator()
{
var key = new Rfc2898DeriveBytes(KEY, Salt, ITERATIONS);
AESManaged.Key = key.GetBytes(AESManaged.KeySize / 8);
}
public byte[] Encrypt(byte[] input)
{
KeyGenerator();
var ms = new MemoryStream();
//Random IV
Encryptor = AESManaged.CreateEncryptor(AESManaged.Key, AESManaged.IV);
//Add the IV to the beginning of the memory stream
ms.Write(BitConverter.GetBytes(AESManaged.IV.Length), 0, sizeof(int));
ms.Write(AESManaged.IV, 0, AESManaged.IV.Length);
var cs = new CryptoStream(ms,
Encryptor, CryptoStreamMode.Write);
cs.Write(input, 0, input.Length);
cs.Close();
return ms.ToArray();
}
public byte[] Decrypt(byte[] input)
{
KeyGenerator();
// Get the initialization vector from the encrypted stream
var ms = new MemoryStream(input);
AESManaged.IV = ReadByteArray(ms);
Decryptor = AESManaged.CreateDecryptor(AESManaged.Key, AESManaged.IV);
var cs = new CryptoStream(ms,
Decryptor, CryptoStreamMode.Write);
cs.Write(input, 0, input.Length);
cs.Close();//Error occurs here
return ms.ToArray();
}
internal static byte[] ReadByteArray(Stream s)
{
var rawLength = new byte[sizeof(int)];
if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
{
throw new SystemException("Stream did not contain properly formatted byte array");
}
var buffer = new byte[16];
if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
{
throw new SystemException("Did not read byte array properly");
}
return buffer;
}
internal static byte[] StreamToByteArray(Stream inputStream)
{
if (!inputStream.CanRead)
{
throw new ArgumentException();
}
// This is optional
if (inputStream.CanSeek)
{
inputStream.Seek(0, SeekOrigin.Begin);
}
var output = new byte[inputStream.Length];
var bytesRead = inputStream.Read(output, 0, output.Length);
Debug.Assert(bytesRead == output.Length, "Bytes read from stream matches stream length");
return output;
}
public void Dispose()
{
if (AESManaged != null)
((IDisposable) AESManaged).Dispose();
}}
Many Thanks in advance
Probably you have solved this already but I'll just put my answer for others who faces similar issue.
Error occurs due to the additional information present in the input array. In public byte[] Encrypt(byte[] input) method you are writing IV length and IV before the ciphered data is written. Lines:
ms.Write(BitConverter.GetBytes(AESManaged.IV.Length), 0, sizeof(int));
ms.Write(AESManaged.IV, 0, AESManaged.IV.Length);
In public byte[] Decrypt(byte[] input) method you are reading this information and using read IV as initialization vector for AES algorithm. All fine. Then you are constructing CryptoStream with CryptoStreamMode.Write and passing MemoryStream object ms which gets decrypted data. However the passed input array contains not only the encrypted message but also the IV that you wrote during the encryption process. That is why it fails to decrypt.
What you need to do to overcome this is either extract only cipher data from the input array and pass it to: cs.Write(cipherData, 0, cipherData.Length); or change mode into CryptoStreamMode.Read and use cs.Read(outputBuff, 0, outputBuff.Length);.
Also don't use the same MemoryStream object to read and write to because you'll have some garbage in it after CryptoStream will write in it.
We had a small desktop app that needs to be provided as a web feature now (.Net). This app contains some code for encryption and uses Rijndael classes from .Net framework. The code accepts an input string, encrypts it and writes it out the results to a file. Since all the code is contained in one class, I just copied the class to my web service application. When I encrypt the same string, using the same key, in the original app and the new app, the results are different.
The result string given by the original app is a subset of the result string given by my web service. The latter has additional characters at the end of the encrypted string.
Below is the code I am using. Please note that I did not develop this code nor do I understand it fully. Any thoughts on the difference in behaviour? Please help!!
Here is the code that gets the user input and calls the encryptor.
public void EncryptDomain(string EncryptValue, string outputDomainFile)
{
if (EncryptValue.Length > 0)
{
if ((outputDomainFile != null) && (outputDomainFile.Length > 0))
{
_outputDomainFile = outputDomainFile;
}
byte[] input = Encoding.UTF8.GetBytes(EncryptValue);
Transform(input, TransformType.ENCRYPT);
}
This is the encryptor code:
private byte[] Transform(byte[] input, TransformType transformType)
{
CryptoStream cryptoStream = null; // Stream used to encrypt
RijndaelManaged rijndael = null; // Rijndael provider
ICryptoTransform rijndaelTransform = null;// Encrypting object
FileStream fsIn = null; //input file
FileStream fsOut = null; //output file
MemoryStream memStream = null; // Stream to contain data
try
{
// Create the crypto objects
rijndael = new RijndaelManaged();
rijndael.Key = this._Key;
rijndael.IV = this._IV;
rijndael.Padding = PaddingMode.Zeros;
if (transformType == TransformType.ENCRYPT)
{
rijndaelTransform = rijndael.CreateEncryptor();
}
else
{
rijndaelTransform = rijndael.CreateDecryptor();
}
if ((input != null) && (input.Length > 0))
{
//memStream = new MemoryStream();
//string outputDomainFile =
FileStream fsOutDomain = new FileStream(_outputDomainFile,
FileMode.OpenOrCreate, FileAccess.Write);
cryptoStream = new CryptoStream(
fsOutDomain, rijndaelTransform, CryptoStreamMode.Write);
cryptoStream.Write(input, 0, input.Length);
cryptoStream.FlushFinalBlock();
//return memStream.ToArray();
return null;
}
return null;
}
catch (CryptographicException)
{
throw new CryptographicException("Password is invalid. Please verify once again.");
}
finally
{
if (rijndael != null) rijndael.Clear();
if (rijndaelTransform != null) rijndaelTransform.Dispose();
if (cryptoStream != null) cryptoStream.Close();
if (memStream != null) memStream.Close();
if (fsOut != null) fsOut.Close();
if (fsIn != null) fsIn.Close();
}
}
Code that sets up the IV values:
private void GenerateKey(string SecretPhrase)
{
// Initialize internal values
this._Key = new byte[24];
this._IV = new byte[16];
// Perform a hash operation using the phrase. This will
// generate a unique 32 character value to be used as the key.
byte[] bytePhrase = Encoding.ASCII.GetBytes(SecretPhrase);
SHA384Managed sha384 = new SHA384Managed();
sha384.ComputeHash(bytePhrase);
byte[] result = sha384.Hash;
// Transfer the first 24 characters of the hashed value to the key
// and the remaining 8 characters to the intialization vector.
for (int loop = 0; loop < 24; loop++) this._Key[loop] = result[loop];
for (int loop = 24; loop < 40; loop++) this._IV[loop - 24] = result[loop];
}
I would guess that's because of the IV (Initialisation Vector)
This is a classic mistake. Whether you generate an IV yourself or not Rijndael (AES) will provide one for you. The trick is to always save the IV (there's a getter on RijndaelManaged).
When you decrypt, you need to pass both the Key and IV.
If you're saving data to a file or database you can store the IV as a plain text. You can even pass the IV on wire (network, internet) as a plain text. The attacker wont be able(as far as I know) break your cipher based just on an IV. Passing or storing the IV is usually done by prefixing it in front of ciphertext or appending it at the end. (concatenating the two strings)
e.g.
CiphertextIV or IVCiphertext. (remember IV is in plaintext is it should be of a fixed length - making it easy to separate upon receiving for decryption or for database insertion)
So, if your Key is ABCDEFABCDEFABCD and your IV is ABCDEF0123456789
and this plaintext:
'this is some secrect text' (let's say) produces a cipher such as: abcd1234abcd00
You would transmit(or store) like it this: ABCDEF0123456789abcd1234abcd00