what if the input key is less than 16 bytes? i found a solution, but its not a best practice.
//hashing here act as padding because any input given, it will generate fixed 20 bytes long.
MessageDigest sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
//trim the code to only 16 bytes.
key = Arrays.copyOf(key, 16);
I'm not planning to use salt because it is not necessary in my project. Is there any better way?
There are three approaches:
Pad the key out to 16 bytes. You can use any value(s) you want to as padding, just so long as you do it consistently.
Your scheme of using a SHA-1 hash is OK. It would be better if you could use all of the bits in the hash as the key, but 128 bits should be enough.
Tell the user that the key needs to be at least N characters. A key that is too short may be susceptible to a password guessing attack. (A 15 character key is probably too long to be guessed, but 8 characters is tractable.) In fact, you probably should do some other password quality checks.
My recommendation is to combine 1. or 2. with 3 ... and password quality checks.
I'm not convinced that seeding the hash will make much difference. (I am assuming that the bad guy would be able to inspect your file encryption app and work out how you turn passwords into keys.) Seeding means that the bad guy cannot pre-generate a set of candidate keys for common / weak passwords, but he still needs to try each of the generated keys in turn.
But the flip-side is that using a crypto hash doesn't help if the passwords you start with are weak.
Don't confuse keys and passwords. Keys are randomly generated and may consist of any possible byte value. Passwords on the other hand need to be typable by a human and usually rememberable. If the key is too short then either emit an error to the user or treat it as a password.
A key should then only be entered in encoded format such as hex or Base64. Only check the length when you successfully decode it.
A password has all kinds of issues that makes it brute forceable such as short length or low complexity. There you would need to use a password-based key derivation function such as PBKDF2 and a sufficiently large work factor (iterations) in order to make a single key derivation attempt so slow that an attacker would need much more time to check the whole input space.
You should combine that with some message to the user to give some hints that the password is too short or doesn't include some character classes and is therefore not recommended.
I am trying to generate hash key using SHA-256 which is producing 64 length String but i need key of size<=32, what is the best algorithm recommended maintaining uniqueness? Please advice.
As already indicated you loose collision resistance for each bit you drop. Hashes however are considered to be indistinguishable from random. Because of the avalanche effect, each bit of the input is "represented" by each of the bits in the hash. So you can indeed simply use the first 128 bits / 16 bytes of the output of the hash. That would still leave you with some 64 bit of collision resistance. The more or less standard way to do this is to take the leftmost bytes.
Additional hints:
To have some additional security, use the result of a HMAC with a static, randomly generated 128 bit key instead of the output of a hash;
Of course you could also encode the hash in base 64 and use that, if you can store any string instead of only hexadecimals. In that case you can fit 32 * 6 = 192 bits into the value, which would result in higher security than a SHA-1 hash (which is considered insecure nowadays, keep to SHA-2).
How would I be able to determine the encryption of a key (AES256 or 3DES 256)...Since both keys will be 32 characters (8 bits per char * 32 char)=256 bits and Mime encoded.
Example
MQAyAEgAOgA5ADUAMwA3AD8AQgBFAD4A --->AES256 key
g1EOWGFb+JjCZ7BbH2RergtKUtDfXrNb --->3DES key
The AES keys were made in Openssl while the 3DES ones were made using Java with the following Apis.
javax.crypto.Cipher;
javax.crypto.KeyGenerator;
javax.crypto.SecretKey;
javax.crypto.SecretKeyFactory;
javax.crypto.spec.DESedeKeySpec;
javax.crypto.spec.IvParameterSpec;
First of all, there is no such thing as 3DES 256. 3DES has a key size of 128 or 192 bits, of which 112 and 168 bits are effectively used. Note that the security margin of 3DES is even lower.
AES on the other hand can be used with 128, 192 and 256 bits, all of which are used.
Now base 64 (not SMIME, that's a higher level protocol) has 6 bits per character (not excluding spurious bits at the end). If I check your keys both of them are 192 bit in size, so that won't help you distinguish the keys. You can use the Apache Codec library to decode base 64 strings.
However, your 3DES key - the second one - seems to use odd parity bytes for the 3 single DES keys. That can be used to distinguish the keys from each other. Note that this is not foolproof, a randomly generated AES key may have the parity bits set correctly by chance alone. However, the chance of that happening is somewhere around the order of 2^24.
It is possible to use the method DESedeKeySpec.isParityAdjusted(byte[] key, int offset) to check if the parity is correctly set. It is required to decode the base 64 string first of course.
Note that sometimes 3DES keys are distributed without having the parity set correctly. In your case, you need to use the KeyFactory to generate the keys otherwise the parity may not be set.
Another way of checking if the key is of the correct type is to decrypt some known plaintext/ciphertext/secretkey pair using both algorithms.
I've got a bunch of 48-bit (6 byte) values that I need to encrypt symmetrically. The two requirements are:
The resulting encrypted value needs to also be 48-bits (6 bytes) long. They key itself can be (and would preferably be) much longer to guard again brute force attacks.
The resulting encrypted value needs to be deterministic, i.e. value A using key B will always produce encrypted value C (we encrypt on the fly and show the encrypted data to the user so need to always show the same value)
All block ciphers I've found have used a minimum block size of 64 and appear to be fixed (you can't use an arbitrary block size). Should I be thinking about a stream cipher?
I'm doing this in Java.
Note: I've seen this question and associated answers but wasn't clear on whether the suggestions would satisfy my 2nd requirement.
Consider format preserving encryption.
(Sorry, I originally misread the requirements thinking it was the INPUT data that needed to be 6 bytes.)
I don't think you can do exactly what you want with standard cryptographic algorithms:
the problem with stream ciphers is that standard ones effectively work by generating a stream of pseudorandom bits from the key and then XORing these bits with the plaintext; effectively this means that you should never use the same stream of bits twice (e.g. if you do, then XORing two ciphertexts gives you the same result as XORing the corresponding plaintexts; and in any case with 48 bits, there are only 2^48 possible bitstreams, so you can just test them all by brute force);
the problem with block ciphers is that there's no standard one as far as I'm aware that has a block size of 48 bits.
Now, that doesn't mean that a 48-bit block cipher couldn't be developed-- and indeed I dare say there are some out there-- just that none of the bog-standard ciphers that have undergone years of scrutiny from the cryptographic community have that block size.
So I would suggest options are:
relax the requirement of a 48-bit ciphertext; for example, TripleDES has a 64-bit block size and is "fairly" secure (equivalent to 112 bit security)[*];
in principle, you could implement your own block cipher with whatever block size you require, sticking as close as you can to a standard design, e.g. a Feistel network following some generally recommended design principles-- as a starting point, see Schneier, "Applied Cryptography", pp. 346ff, "Theory of Block Cipher Design".
The obvious problem with the latter option is that, whist standard block ciphers are generally based on common general principles, they adopt particular design decisions that have been subject to considerable scrutiny; yours presumably won't be.
I would also recommend standing back a bit from the problem (or perhaps explaining a bit more what you're trying to do), because it seems to be based on requirements that would normally go against good security practice (having the same plaintext always encrypt to the same ciphertext is something one would normally specifically avoid, for example). So you could have the best designed Feistel cipher in the world, but introduce some other vulnerability in how you're using it.
[*] TripleDES is generally not recommended because AES gives better security more efficiently (you might like to see some comparative timings of block ciphers that I took in Java to see just how bad it is). However, this might not matter in your particular application.
No, just "pad" your data out with some bytes you don't care about (but which are always the same if that's your requirement) so that you reach the size of a block. (If you're using an appropriate padding mode, then this will be done for you.)
I believe this is what you are looking for
http://web.cs.ucdavis.edu/~rogaway/papers/shuffle.html
This algorithm lets you construct PRP (i.e. arbitrary length block cipher) from secure PRF (e.g. sha256, blake2)
Block cipher in CTR mode has the same issue as a stream cipher.
Without a proper MAC (which require more bytes added) it will susceptible to bit flipping.
And without unique IV (which also require more bytes added) it will be just a broken implementation.
You can use a stream cipher only, if you have a unique salt for every encryption (don't even think about re-using the same salt, as that would be trivial to break).
When you have such unique values (e. g. a sequence number that's already associated with your values), you can use e.g. the stream cipher RC4-drop.
When you don't have such unique numbers already, you probably can't use a stream cipher, because you only have 48 bits for your result (so no space left for the salt.)
As for a block cipher with 48 bits - sorry, I don't know such a cipher, either. Maybe what you can do, is combining four 48 bit values into a 192 bit value, resulting in three 64 bit blocks, encode them, and then split them into four 48 bit values again. (I have no idea, if that would be possible in your situation or not?)
If you have a unique counter / sequence number associated with each plaintext value, then you can just use any block cipher in CTR (counter) mode.
To encrypt value V that is sequence number N under key K:
Expand N to the size of a block;
Encrypt N with the block cipher and key K;
Take the first 48 bits of the result and XOR them with V.
Decryption is the same. The most important thing to remember with this method is:
Never, ever use the same key and sequence number to encrypt two different values.
Sequence numbers must be unique. If your sequence restarts, you must use a new key.
There is a 48-bit block 80-bit key cipher designed in 2009 - KATAN48 (the family version of KTANTAN48 has some key scheduling issue. So far, it was not broken, and has quite high security margins, so it has passed the test of time.
Here's a proposed solution that meets both of your requirements.
What if you use a cipher with a 32-bit block size (such as Skip32) and run it twice, as described below.
You have a 48-bit value to encode, for example:
f2800af40110
Step 1:
Split this into a 32-bit value and a 16-bit value using some method. Here we'll just grab the left 4 bytes and the right 2 bytes (but in practice you could use a secret bitmask, see below).
32-bit value: f2800af4
16-bit value: 0110
Step 2:
Encrypt the first one with a secret key K1, using Skip32 and let's say we get:
Encrypted 32-bit value: b0daf2b9
Step 3:
Split this into two 16-bit values (again you could use a secret bitmask, but for this example we'll grab the left/right two bytes).
Value 1: b0da
Value 2: f2b9
Step 4:
Combine value 1 with the the 16-bit value from step 1 to get a new 32-bit value:
b0da0110
Step 5:
Encrypt the resulting 32-bit value with secret key K2, again using Skip32:
Encrypted 32-bit value: 6135d8f4
Step 6:
Combine this 32-bit value with value 2 from step 3 to get a 48-bit encrypted result.
6135d8f4f2b9
The result is both deterministic and reversible. No two inputs will produce the same output.
Note on splitting/combining values
Steps 1 and 3 above will split a value in a predictable way. I'm not sure if this introduces any weakness, but one alternative is to use a bitmask. If we want to split a 48-bit input number into 32-bits and 16-bits, we could come up with a bitmask, essentially a number with 16 1's that can dictate how we split bits of the input number to get two output numbers, as follows:
INPUT : 111100101000000000001010111101000000000100010000
BITMASK: 001010111001110000100000010001100000011001100000
| | ||| ||| | | || || ||
VALUE 1: 1 0 101 000 0 1 10 00 00 => a860
VALUE 2: 11 1 0 00 0000 010101 110 000000 10 10000 => e015c050
Similarly for steps 4 and 6 you could combine two values by interleaving bits based on a bitmask.
If you use separate bitmasks for each step and separate keys for each step you end up with 2 keys and 4 bitmasks, all of which would be needed to encrypt/decrypt values.
Quick note on Skip32 and it's use cases
"The Skip32 has the uncommon properties of being fast, creating very dissimilar encrypted values for consecutive input values, and producing output of the same size as the input. These make this cipher particularly useful for obfuscating series of sequential integers (e.g. auto-incremented database ids)."
- https://docs.rs/skip32/1.0.5/skip32/
Any thoughts about this approach from someone more experienced with cryptography than I am?
I have already read Using Java to encrypt integers and Encrypting with DES Using a Pass Phrase.
All I need is a simple Encrypter which transforms a 12 digit number to a 12 digit number with the following constraints:
The encryption must depend on a password (which will be constant throughout the life time of an application) and nothing else.
The mapping must be 1-1 (No hashing and multiple inputs giving same output and vice versa).
The mapping must not change between different VMs or when VM is started (like when you restart Java, the utility should give you same mappings which means that it must be purely dependent on the password that is supplied).
Numbers starting with 0 is not a valid 12 digit number (also input numbers won't start with 0).
The key/password should never be guessable. For example running the utility with multiple inputs and analysing the outputs should not allow one to guess the key/pwd/hash or whatever.
All inputs will be exactly 12 digits and less than a 12 digit prime number (which means we could use modulo arithmetic).
Having trawled through the literature I have this code with me
public void mytestSimple(long code, String password) throws Exception {
SecretKey key = new SecretKeySpec(password.getBytes(), "DES");
Cipher ecipher = Cipher.getInstance("DES");
ecipher.init(Cipher.ENCRYPT_MODE, key);
System.out.println(ecipher.getOutputSize(8));
byte[] encrypted = ecipher.doFinal(numberToBytes(code));
System.out.println(encrypted + "--" + encrypted.length);
Cipher dcipher = Cipher.getInstance("DES");
dcipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = dcipher.doFinal(encrypted);
System.out.println(bytesToNumber(decrypted) + "--" + decrypted.length);
}
public void testSimple() throws Exception {
mytestSimple(981762654986L, "password");
}
I am running into problems as to
How to convert the 16 bytes into a 12 digit number.
Maintain 1-1 mapping.
Keep the encryption/decryption same across multiple VM invocations.
**** Answer added by me below****
I have added one answer which is a 40bit RSA pulled out of standard Java RSA keypair gen logic. I still have to work on the edge cases. I am going to accept the answer and upvote "Tadmas" who I think kinda lead me to the answer. Can someone tell me if my algorithm is going to be weak/attackable?
You're not going to be able to convert 16 bytes into a 12 digit number without losing information. 256 ^ 16 > 10^12. (Not that you even have 10^12 options, as you've only got the range [100000000000, 999999999999].
I doubt that you'll be able to use any traditional encryption libraries, as your requirements are somewhat odd.
If the strict 1:1 mapping is more important than protecting against cryptanalysis, then you can convert the password to a 12-digit number (via hash or otherwise) and simply add to your original number mod 10^12. If you absolutely must remove leading zeros from the output, you can subtract 10^11, do the math mod (10^12 - 10^11), and then add 10^11 back again. Granted, that's extremely insecure, but it's quite simple. :)
If the range of inputs is bounded by a prime less than (10^12 - 10^11), you may be able to use message ^ password mod prime to form a ring that will satisfy your requirements and be a little harder to crack. (This is similar to how RSA works.) I think this could work if you don't need to decrypt it.
I agree with Jon Skeet: requiring a strict 1:1 mapping without the output range being bigger than the input domain is something that most encryption libraries are not going to handle.
One potential solution could be built on Feistel ciphers.
This constructions allows to build a pseudorandom permutation based on a pseudorandom functions. E.g. the pseudorandom functions could be constructed from an appropriate block cipher by truncating the result to a 6 digit numbers.
This construction has been analyzed in the following paper
M. Luby and C. Rackoff, "How to construct pseudorandom permutations from pseudorandom functions" SIAM Journal on Computing, Vol.17, No.2, pp.373--386, 1988
A concrete proposal is the Feistel Finite Set Encryption Mode, which has been
submitted to NIST for potential inclusion into an upcoming standard. This proposal also
addresses the problem of encrypting ranges that are not a power of 2.
If the numbers are for user IDs, this is what I'd do:
(1) Generate an AES key from the password. Just calling getBytes() is sort of OK if you trust the administrator to use a really really really strong password. Ideally, use the standard "password-based encryption" technique of hashing the bytes, say, a few thousand times, each time adding in the random "salt" bytes that you initially generated to avoid dictionary attacks.
(2) Encrypt the number in question with that AES key.
(3) Chop off 12 digits' worth of bits from the resulting encrypted block, convert it to decimal, and present that number to the user. (To do this, you can wrap a BigInteger around the bytes, call toString() on it, and pull off, say, the bytes between position 4 and 16.) Experimentally, it looks like you shouldn't take the digits from the rightmost end.
[Update: I think this is probably because BigInteger literally allocates its numbers from left to rightmost bit-- but I haven't checked-- so there'll potentially be "spare" bits in the very rightmost byte, and hence fewer possible numbers if you include the very last byte.]
Now, I hear you cry, this obviously isn't a 1-1 mapping. But unless you're going to have more than tens of thousands of users, it's really good enough. With a 12-digit number, you'd expect on average to encrypt around 300,000 numbers before getting a collision. So although you don't strictly have a 1-1 mapping, in practice, it's as near as dammit.
(In any case, if your application really has hundreds of thoudands of users and security is crucial, then you'll probably want to invest in some serious consulting over this kind of thing...)
Just to convince yourself that it really is OK to pretend it's a 1-1 mapping, you can run a simulation that repeatedly tries to allocate, say, 200,000 user IDs with random keys, and prints out how many collisions there were on each run:
next_pass :
for (int pass = 0; pass < 100; pass++) {
byte[] key = new byte[16];
(new SecureRandom()).nextBytes(key);
Cipher ciph = Cipher.getInstance("AES");
SecretKeySpec ks = new SecretKeySpec(key, "AES");
ByteBuffer bb = ByteBuffer.allocate(16);
Set<String> already = new HashSet<String>(100000);
int colls = 0;
for (int i = 0; i < 200000; i++) {
bb.putLong(0, i);
ciph.init(Cipher.ENCRYPT_MODE, ks);
byte[] encr = ciph.doFinal(bb.array());
encr[0] &= 0x7f; // make all numbers positive
BigInteger bigint = new BigInteger(encr);
String userNo = bigint.toString();
userNo = userNo.substring(4, 16);
if (!already.add(userNo)) {
System.out.println("Coll after " + i);
continue next_pass;
}
}
System.out.println("No collision.");
}
I suggest a very simple algorithm.
Feed the password into a hash function.
Initialize a random number generator with the hash or something you derived from the hash.
Generate a 12 digit random number.
Add this random number to the input digit by digit modulo 10 to encrypt.
To decrypt subtract the random number modulo 10. This is actually a form of One Time Pad. Because of the comments on this answer I realized that refering to One Time Pad was a bad choice. A better reference is Polyalphabetic cipher - while One Time Pad uses polyalphabetic substitution its main characteristic is not to use any key bit twice.
Input 1234 1234 1234
Random number 6710 3987 2154
Output 7944 4111 3388
There is one remaining problem with that - the algorithm might create leading zeros. To solve this problem one could use XOR instead of addition and substraction. Just transform the digits with XOR. If the first digit turns into a zero, don't encrypt the first digit. When you decrypt with XOR again, the first digit will turn into zero and you know that the first digit was not enrcypted.
UPDATE
A simple XOR is not the solution because it will produce to large numbers - 2 XOR 9 = 11 for example. Going to rethinks this...
UPDATE
The nice propoerties of XOR are XOR(a, b) = XOR(b, a) and XOR(XOR(a, b), b) = a. This makes encryption and decryption the same and allows to detect the unencrypted leading digit. But it is further required that our function only returns values in the range from 0 to 9 what XOR doesn't do.
But maybe we can build a custom function with all required properties. So we create an array FUNC with 10 columns and 10 rows and use it as a lookup table for our function. What values to but in? I actually don't know - I am not even sure that it is possible. But if we pick three number from the range 0 to 9 we have to make the following six entries. (It is a symmetric matrix.)
FUNC[x,y] = z FUNC[x,z] = y FUNC[y,z] = x
FUNC[y,x] = z FUNC[z,x] = y FUNC[z,y] = x
So maybe it is possible to create such a function by repeatedly choosing random numbers and filling the six entries if there is no conflict. Maybe it is not. I would like to see the table if one finds a solution.
Me thinks the answer given below by Tadmas was very helpful and I want you guys to hack/bully my implementation below. As Tadmas points out all my numbers are 40 bits (12 digit number is 10^12 which is 2^40 approx).
I copied the sun.security.rsa.RSAKeyPairGenerator (link) and created my own generator for a 40 bit RSA algorithm. The standard one needs between 512-1024 bits so I removed the input check around it. Once I create a suitable n, e, d values (e seems to be 65537 as per the alog). The following code served fine,
public void testSimple() throws NoSuchAlgorithmException {
MyKeyPairGenerator x = new MyKeyPairGenerator();
x.initialize(40, new SecureRandom("password".getBytes()));
MyPublicPrivateKey keypair = x.generateKeyPair();
System.out.println(keypair);
BigInteger message = new BigInteger("167890871234");
BigInteger encoded = message.modPow(keypair.e, keypair.n);
System.out.println(encoded); //gives some encoded value
BigInteger decoded = encoded.modPow(keypair.d, keypair.n);
System.out.println(decoded); //gives back original value
}
Disadvantages
The encoded may not always be 12 digits (sometimes it may start with 0 which means only 11 digits). I am thinking always pad 0 zeroes in the front and add some CHECKSUM digit at the start which might alleviate this problem. So a 13 digit always...
A 40 bits RSA is weaker than 512 bit (not just 512/40 times but an exponential factor of times). Can you experts point me to links as to how secure is a 40bit RSA compared to 512 bit RSA (I can see some stuff in wiki but cannot concretely confirm possibility of attacks)? Any links (wiki?) on probabilities/number of attempts required to hack RSA as a function of N where n is the number of bits used will be great !
I would use a stream cipher. N bytes go in, N bytes come out.
This thread is 4 years old, but for those finding the thread in Google: have a look at Format Preserving Ciphers: http://en.wikipedia.org/wiki/Format-preserving_encryption
For mathematical reasons, most cyphers will produce "more" bytes (i.e. they will pad the input). So you will have to accept that the code generates 16bytes out of your 12 digit number.
When the string is decoded, you will get the 12 digit number back but during transport, you need 16 bytes.
If I understand your problem correctly you have a problem with the 16 bytes after decrypting. The trick is to use: Cipher.getInstance("DES/ECB/PKCS5Padding");
(just in case code is helpful):
public void mytestSimple(long code, String password) throws Exception
{ SecretKey key = new SecretKeySpec (password.getBytes(),"DES");
Cipher ecipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
byte[] plaintext = new byte[8];
for (int i=0; i<8; i++)
{ plaintext[7-i] = (byte) (code & 0x00FF);
>>>= 8;
}
ecipher.init (Cipher.ENCRYPT_MODE, key);
System.out.println(ecipher.getOutputSize(8));
byte[] encrypted = ecipher.doFinal(plaintext);
System.out.println("--" + encrypted.length);
Cipher dcipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
dcipher.init(Cipher.DECRYPT_MODE, key);
byte[] crypttext = dcipher.doFinal(encrypted);
long decoded = 0;
for (int i=0; i<8; i++)
{ decoded <<= 8;
decoded += crypttext[i] & 0x00FF;
}
System.out.println(decode + "--" + crypttext.length);
}
Rethinking the problem I came up with the following. Basicly you need a symmetric cipher to get a one-to-one mapping. And noting that 10^12 is almost 2^40 (2^39.863) it seems natural to convert your 12 digit number into a 40 bit integer and feed this number into a block cipher with a block length of 40 bits. A good choice might be Blowfish supporting block lengths from 32 to 448 bits in steps of 8 bits - so 40 bits is supported.
UPDATE
As Accipitridae pointed out, Blowfish has variable key size but fixed block size hence it is no option. A bit more searching through the web seems to indicate, that there are little or no ciphers with block sizes of 40 bits or less rendering this idea void. A leave the remaining part of the answer - maybe one can find a suitable cipher.
The remaining problem is that the Blowfish might return a number up to 1,099,511,627,775 with 13 digits and that the returned number might contain leading zeros, but I believe that this can be solved in a second step. My first thought was applying something like a Burrows-Wheeler transform to the string representation of the number in order to get at least one zero to the front of the number eleminating the 13th digit and then modify all remaining digits (for example 0 <-> 9, 1 <-> 8, 2 <-> 7, ...) to turn additional leading zeros into other digits.
After a few minutes I regonized that this will not work - the input has size 2^40 while the output is only of size 2^39.863. A solution would be to use a 39 bits block cipher and restrict the input to numbers up to 549,755,813,887. I don't know if there is a cipher that can deal with a block length of 39 bits, but this paper on Elastic Block Ciphers describes how to construct a block cipher that can deal with every block size from n up to 2n given a block cipher that can handle block size n. In consequence it is possible to construct a 39 bit block cipher from 32 bit Blowfish.
If you're prepared to accept a rather weak solution...
Treat the 12-digit number as two 6-digit numbers. Then use the hash of the password as a seed to a random number generator which shuffles a table of 999,990 consecutive values. Then use the two 6-digit numbers to look up entries in the table. The concatenation of the two results is your 1:1 mapped 12-digit 'encrypted' input based on a password.
Let's do an example with 4-digits instead of 12...
Input: 4852
Password: passw0rd1 => hashcode = 37592
Now take this array..
a = [10, 11, 12, 13, .. 98, 99]
And shuffle it with 37592 as the random seed...
b = [45, 15, 56, 49, .. 33, 88]
Now split the input: 48, 52 and look up those indices in the shuffled array, say...
b[48] => 23
b[52] => 96
So our encrypted version of 4852 is 2396
It really isn't a strong solution but the constraints in your question will not lead to a strong solution. You may need to relax those constraints a bit.