I have a Java Application that uses "AES-128 bits/ECB/PKCS5Padding" (java8 linux/window), the code is quite simple
KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(seed.getBytes());
keygen.init(128, secureRandom);
SecretKey secretKey = keygen.generateKey();
...
Because I can't find the javascript equivalent to SHA1PRNG algorithm I can't decrypt the text using js code. But after reading Decrypt AES/CBC/PKCS5Padding with CryptoJS and with some trials I found that for an 128 bits seed (32 bits hex-string) using SHA1PRNG in java I can get the same result by SHA1 twice in js
CryptoJS.SHA1(CryptoJS.SHA1(seed)).toString().substring(0, 32) //using 'crypto-js'
The python code here also confirms that! But why ?
def get_sha1prng_key(key):
'''[summary]
encrypt key with SHA1PRNG
same as java AES crypto key generator SHA1PRNG
Arguments:
key {[string]} -- [key]
Returns:
[string] -- [hexstring]
'''
signature = hashlib.sha1(key.encode()).digest()
signature = hashlib.sha1(signature).digest()
return ''.join(['%02x' % i for i in signature]).upper()[:32]
---- update ----
The comments I got suggested my question is a duplicated question. But I checked those 2 questions and I don't think so. But first of all, I knew the java codes misuse a pseudo random number generator and it is seed as a key derivation function, it is bad. But that is actually someone else codes and my job is to use js to decrypt the encrypted text.
Second, I haven't figured out why sha1 a 32bit hex-string twice will get the same result as java 8 SHA1PRNG sun implementation(and hence the question).
I read Use of "SHA1PRNG" in SecureRandom Class
"SHA1PRNG" is the name of a pseudo random number generator (the PRNG in the name). That means that it uses the SHA1 hash function to generate a stream of random numbers... There is no clear description of the algorithm available
To my knowledge, there is no document that specifies the SHA1PRNG algorithm. The closest thing to a specification is Sun's implementation. Subsequent providers have made their own changes. Therefore, the SHA1PRNG algorithm is implemented non-uniformly across platforms (and sometimes even across versions) as is the case e.g. for Android and Sun.
Since the question is about Sun's SHA1PRNG implementation, the most reliable solution is to port the Sun logic. A possible lightweight port based on JDK 24 is:
class Sha1PrngLW {
static DIGEST_SIZE = 20;
constructor(seed) {
this.state = Sha1PrngLW.sha1(seed);
this.remainder = new Uint8Array(Sha1PrngLW.DIGEST_SIZE);
this.remCount = 0;
}
getNextBytes(number) {
const result = new Uint8Array(number);
let index = 0;
let todo;
let output = this.remainder;
let r = this.remCount;
if (r > 0) {
todo = Math.min(result.length - index, Sha1PrngLW.DIGEST_SIZE - r);
for (let i = 0; i < todo; i++) {
result[i] = output[r];
output[r++] = 0;
}
this.remCount += todo;
index += todo;
}
while (index < result.length) {
output = Sha1PrngLW.sha1(this.state);
this.updateState(this.state, output);
todo = Math.min((result.length - index), Sha1PrngLW.DIGEST_SIZE);
for (let i = 0; i < todo; i++) {
result[index++] = output[i];
output[i] = 0;
}
this.remCount += todo;
}
this.remainder = output;
this.remCount %= Sha1PrngLW.DIGEST_SIZE;
return result;
}
updateState(state, output) {
let last = 1;
let v;
let t;
let zf = false;
for (let i = 0; i < state.length; i++) {
v = Sha1PrngLW.toSByte(state[i]) + Sha1PrngLW.toSByte(output[i]) + last;
t = v & 0xFF;
zf = zf || (state[i] !== t);
state[i] = t;
last = v >> 8;
}
if (!zf) {
state[0]++;
}
}
static toSByte(byte) {
return byte > 127 ? byte - 256 : byte;
}
static sha1(data) {
return new Uint8Array(sha1.create().update(data).arrayBuffer()); // apply your preferred SHA-1 implementation
}
}
The code above uses the js-sha1 implementation for SHA-1, which can of course be replaced by one of your preference (e.g. CryptoJS).
Use case - Creation of a 32 bytes key for AES-256:
class Sha1PrngLW {
static DIGEST_SIZE = 20;
constructor(seed) {
this.state = Sha1PrngLW.sha1(seed);
this.remainder = new Uint8Array(Sha1PrngLW.DIGEST_SIZE);
this.remCount = 0;
}
getNextBytes(number) {
const result = new Uint8Array(number);
let index = 0;
let todo;
let output = this.remainder;
let r = this.remCount;
if (r > 0) {
todo = Math.min(result.length - index, Sha1PrngLW.DIGEST_SIZE - r);
for (let i = 0; i < todo; i++) {
result[i] = output[r];
output[r++] = 0;
}
this.remCount += todo;
index += todo;
}
while (index < result.length) {
output = Sha1PrngLW.sha1(this.state);
this.updateState(this.state, output);
todo = Math.min((result.length - index), Sha1PrngLW.DIGEST_SIZE);
for (let i = 0; i < todo; i++) {
result[index++] = output[i];
output[i] = 0;
}
this.remCount += todo;
}
this.remainder = output;
this.remCount %= Sha1PrngLW.DIGEST_SIZE;
return result;
}
updateState(state, output) {
let last = 1;
let v;
let t;
let zf = false;
for (let i = 0; i < state.length; i++) {
v = Sha1PrngLW.toSByte(state[i]) + Sha1PrngLW.toSByte(output[i]) + last;
t = v & 0xFF;
zf = zf || (state[i] !== t);
state[i] = t;
last = v >> 8;
}
if (!zf) {
state[0]++;
}
}
static toSByte(byte) {
return byte > 127 ? byte - 256 : byte;
}
static sha1(data) {
return new Uint8Array(sha1.create().update(data).arrayBuffer()); // apply your preferred SHA-1 implementation
}
}
var seed = hex2ab("0099447669d26c259ab0bb399cdf6d40c80afca162bfb7aae8354c9ba6fb594d2d323657c958763a19407f0be2e6cc23");
var prng = new Sha1PrngLW(seed);
var nextBytes = prng.getNextBytes(32);
console.log(ab2hex(nextBytes)); // e92c65d5e0ab7400578756d62920f7f5206a70f5760c40819ed7c27682347c87
var prng = new Sha1PrngLW(seed);
var nextBytes1 = prng.getNextBytes(10);
var nextBytes2 = prng.getNextBytes(22);
console.log(ab2hex(nextBytes1) + ab2hex(nextBytes2)); // e92c65d5e0ab7400578756d62920f7f5206a70f5760c40819ed7c27682347c87
// Helper
function ab2hex(ab) {
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
function hex2ab(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha1/0.7.0/sha1.min.js"></script>
The results obtained correspond to those of the Java code (tested with Java 8). Also note that the first 20 bytes correspond to the double SHA-1 hash of the seed.
Why and under what conditions does this logic correspond to double SHA-1 hashing?
Looking at the code, you can see that first an initial SHA-1 hashing of the seed is performed in the constructor. The hash is stored in state
.
In the while
-loop, state
is hashed a second time with SHA-1. The result is stored in output
, the length of which corresponds to the SHA-1 output length of 20 bytes.
Then state
is updated for the next iteration step in updateState()
. However, the next iteration only takes place if the specified output length result.Length
(= number
) is greater than the SHA-1 output length, i.e. 20 bytes. As long as this is not the case, there is only one iteration step and the logic simplifies to a double hashing with SHA-1.
Since your use case applies AES-128, i.e. a key length of 16 bytes, which is smaller than the SHA-1 output length, the same key can therefore also be generated by double hashing with SHA-1 and then shortening the byte sequence to 16 bytes.
Of course, this does not work for AES-256, as its key length of 32 bytes is greater than the SHA-1 output length of 20 bytes.
For the sake of completeness: The Sun implementation allows the byte sequence to be determined in multiple getNextBytes()
calls, i.e. for n = n1 + n2
, getNextBytes(n)
corresponds to the concatenation of getNextBytes(n1)
and getNextBytes(n2)
. This is the reason for the additional complexity of the implementation (which requires e.g. remainder
, remCount
and the if (r > 0)
-branch).