Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2002-2026 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.javascript.host.crypto;

import java.util.Set;

import org.htmlunit.corejs.javascript.NativeObject;
import org.htmlunit.corejs.javascript.ScriptRuntime;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.ScriptableObject;
import org.htmlunit.corejs.javascript.TopLevel;

/**
* Internal helper representing AES key algorithm parameters.
* Used by {@link SubtleCrypto} for AES key operations.
*
* @author Lai Quang Duong
*/
final class AesKeyAlgorithm {

static final Set<String> SUPPORTED_NAMES = Set.of("AES-CBC", "AES-CTR", "AES-GCM", "AES-KW");
static final Set<Integer> SUPPORTED_LENGTHS = Set.of(128, 192, 256);

private final String name_;
private final int length_;

AesKeyAlgorithm(final String name, final int length) {
if (!SUPPORTED_NAMES.contains(name)) {
throw new UnsupportedOperationException("AES " + name);
}
name_ = name;

if (!SUPPORTED_LENGTHS.contains(length)) {
throw new IllegalArgumentException("Data provided to an operation does not meet requirements");
}
length_ = length;
}

/**
* Parse AES key algorithm parameters from a JS object.
*
* @param keyGenParams the JS algorithm parameters object
* @return the parsed AesKeyAlgorithm
*/
static AesKeyAlgorithm from(final Scriptable keyGenParams) {
final Object nameProp = ScriptableObject.getProperty(keyGenParams, "name");
if (!(nameProp instanceof String name)) {
throw new IllegalArgumentException("An invalid or illegal string was specified");
}

final Object lengthProp = ScriptableObject.getProperty(keyGenParams, "length");
if (!(lengthProp instanceof Number numLength)) {
throw new IllegalArgumentException("An invalid or illegal string was specified");
}

return new AesKeyAlgorithm(name, numLength.intValue());
}

static boolean isSupported(final String name) {
return SUPPORTED_NAMES.contains(name);
}

String getName() {
return name_;
}

int getLength() {
return length_;
}

/**
* Converts to a JS object matching the {@code AesKeyAlgorithm} dictionary:
* {@code {name: "AES-GCM", length: 256}}
*
* @param scope the JS scope for prototype/parent setup
* @return the JS algorithm object
*/
Scriptable toScriptableObject(final Scriptable scope) {
final NativeObject algorithm = new NativeObject();

Check warning on line 91 in src/main/java/org/htmlunit/javascript/host/crypto/AesKeyAlgorithm.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Avoid using implementation types like 'NativeObject'; use the interface instead Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/crypto/AesKeyAlgorithm.java"},"region":{"endColumn":27,"endLine":91,"startColumn":15,"startLine":91}}}],"message":{"text":"Avoid using implementation types like 'NativeObject'; use the interface instead"},"ruleId":"LooseCoupling","ruleIndex":166}
ScriptRuntime.setBuiltinProtoAndParent(algorithm, scope, TopLevel.Builtins.Object);
ScriptableObject.putProperty(algorithm, "name", getName());
ScriptableObject.putProperty(algorithm, "length", getLength());
return algorithm;
}
}
109 changes: 108 additions & 1 deletion src/main/java/org/htmlunit/javascript/host/crypto/CryptoKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,131 @@
*/
package org.htmlunit.javascript.host.crypto;

import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

import javax.crypto.SecretKey;

import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxGetter;
import org.htmlunit.javascript.host.Window;

/**
* A JavaScript object for {@code CryptoKey}.
*
* @see <a href="https://w3c.github.io/webcrypto/#dfn-CryptoKey">CryptoKey</a>
*
* @author Ahmed Ashour
* @author Ronald Brill
* @author Lai Quang Duong
*/
@JsxClass
public class CryptoKey extends HtmlUnitScriptable {

private Key internalKey_;
private String type_;
private boolean isExtractable_;
private Scriptable algorithm_;
private Set<String> usages_;

/**
* JavaScript constructor.
*/
@JsxConstructor
public void jsConstructor() {
// nothing to do
throw JavaScriptEngine.typeErrorIllegalConstructor();
}

/**
* Creates a properly scoped CryptoKey from the given parameters.
*
* @param scope the JS scope
* @param internalKey the Java key (SecretKey, PublicKey, or PrivateKey)
* @param isExtractable whether the key can be exported
* @param algorithm the JS algorithm descriptor object
* @param usages the permitted key usages
* @return the new CryptoKey
*/
static CryptoKey create(final Scriptable scope, final Key internalKey, final boolean isExtractable,
final Scriptable algorithm, final Collection<String> usages) {
final CryptoKey key = new CryptoKey();
key.internalKey_ = Objects.requireNonNull(internalKey);

if (internalKey instanceof PublicKey) {
key.type_ = "public";
}
else if (internalKey instanceof PrivateKey) {
key.type_ = "private";
}
else if (internalKey instanceof SecretKey) {
key.type_ = "secret";
}
else {
throw new IllegalStateException("Unsupported key type: " + internalKey.getClass());
}

key.isExtractable_ = isExtractable;
key.algorithm_ = algorithm;
key.usages_ = new LinkedHashSet<>(usages);

final Window window = getWindow(Objects.requireNonNull(scope));
key.setParentScope(window);
key.setPrototype(window.getPrototype(CryptoKey.class));
return key;
}

/**
* @return the Java key (opaque {@code [[handle]]} internal slot)
*/
public Key getInternalKey() {
return internalKey_;
}

/**
* @return the key type: "public", "private", or "secret"
*/
@JsxGetter
public String getType() {
return type_;
}

/**
* @return whether the key material may be exported
*/
@JsxGetter
public boolean getExtractable() {

Check warning on line 118 in src/main/java/org/htmlunit/javascript/host/crypto/CryptoKey.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 A getX() method which returns a boolean or Boolean should be named isX() Raw Output: {"level":"note","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/crypto/CryptoKey.java"},"region":{"endColumn":34,"endLine":118,"startColumn":20,"startLine":118}}}],"message":{"text":"A getX() method which returns a boolean or Boolean should be named isX()"},"ruleId":"BooleanGetMethodName","ruleIndex":214}
return isExtractable_;
}

/**
* @return the algorithm descriptor object
*/
@JsxGetter
public Scriptable getAlgorithm() {
return algorithm_;
}

/**
* @return the permitted key usages as a JS array
*/
@JsxGetter
public Scriptable getUsages() {
return JavaScriptEngine.newArray(this, usages_.toArray());
}

/**
* @return the permitted key usages as a Java set (for internal use)
*/
public Set<String> getUsagesInternal() {
return usages_;
}
}
110 changes: 110 additions & 0 deletions src/main/java/org/htmlunit/javascript/host/crypto/EcKeyAlgorithm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2002-2026 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.javascript.host.crypto;

import java.util.Map;
import java.util.Set;

import org.htmlunit.corejs.javascript.NativeObject;
import org.htmlunit.corejs.javascript.ScriptRuntime;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.ScriptableObject;
import org.htmlunit.corejs.javascript.TopLevel;

/**
* Internal helper representing EC key algorithm parameters.
* Used by {@link SubtleCrypto} for ECDSA and ECDH key operations.
*
* @author Lai Quang Duong
*/
final class EcKeyAlgorithm {

static final Set<String> SUPPORTED_NAMES = Set.of("ECDSA", "ECDH");

private static final Map<String, String> CURVE_TO_JCA = Map.of(
"P-256", "secp256r1",
"P-384", "secp384r1",
"P-521", "secp521r1"
);

private final String name_;
private final String namedCurve_;

EcKeyAlgorithm(final String name, final String namedCurve) {
if (!SUPPORTED_NAMES.contains(name)) {
throw new UnsupportedOperationException("EC " + name);
}
name_ = name;

if (!CURVE_TO_JCA.containsKey(namedCurve)) {
throw new UnsupportedOperationException("EC curve " + namedCurve);
}
namedCurve_ = namedCurve;
}

/**
* Parse EC key algorithm parameters from a JS object.
*
* @param keyGenParams the JS algorithm parameters object
* @return the parsed EcKeyAlgorithm
*/
static EcKeyAlgorithm from(final Scriptable keyGenParams) {
final Object nameProp = ScriptableObject.getProperty(keyGenParams, "name");
if (!(nameProp instanceof String name)) {
throw new IllegalArgumentException("An invalid or illegal string was specified");
}

final Object curveProp = ScriptableObject.getProperty(keyGenParams, "namedCurve");
if (!(curveProp instanceof String namedCurve)) {
throw new IllegalArgumentException("An invalid or illegal string was specified");
}

return new EcKeyAlgorithm(name, namedCurve);
}

static boolean isSupported(final String name) {
return SUPPORTED_NAMES.contains(name);
}

String getName() {
return name_;
}

String getNamedCurve() {
return namedCurve_;
}

/**
* @return the JCA curve name (e.g. "secp256r1" for "P-256")
*/
String getJavaCurveName() {
return CURVE_TO_JCA.get(namedCurve_);
}

/**
* Converts to a JS object matching the {@code EcKeyAlgorithm} dictionary:
* {@code {name: "ECDSA", namedCurve: "P-256"}}
*
* @param scope the JS scope for prototype/parent setup
* @return the JS algorithm object
*/
Scriptable toScriptableObject(final Scriptable scope) {
final NativeObject algorithm = new NativeObject();

Check warning on line 104 in src/main/java/org/htmlunit/javascript/host/crypto/EcKeyAlgorithm.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Avoid using implementation types like 'NativeObject'; use the interface instead Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/crypto/EcKeyAlgorithm.java"},"region":{"endColumn":27,"endLine":104,"startColumn":15,"startLine":104}}}],"message":{"text":"Avoid using implementation types like 'NativeObject'; use the interface instead"},"ruleId":"LooseCoupling","ruleIndex":166}
ScriptRuntime.setBuiltinProtoAndParent(algorithm, scope, TopLevel.Builtins.Object);
ScriptableObject.putProperty(algorithm, "name", getName());
ScriptableObject.putProperty(algorithm, "namedCurve", getNamedCurve());
return algorithm;
}
}
Loading
Loading