Skip to content
Open
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
42 changes: 42 additions & 0 deletions examples/webcrypto/derive_bits/derive-bits-pbkdf2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function stringToArrayBuffer(str) {
const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
const bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}

const printArrayBuffer = (buffer) => {
const view = new Uint8Array(buffer);
return Array.from(view);
};

export default async function () {
// create a low entropy password as array of bytes
const password = stringToArrayBuffer("hello world password!")

// import password as CryptoKey
const importedKey = await crypto.subtle.importKey('raw', password, 'pbkdf2', false, [
'deriveBits', 'deriveKey'
]);

// generate random salt
const saltArray = new Uint8Array(16)
crypto.getRandomValues(saltArray)
const salt = saltArray.buffer

// derive base key bits using pbkdf2
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
hash: "SHA-256",
salt,
iterations: 25384
},
importedKey,
256
)

console.log("Derived Bits: ", printArrayBuffer(derivedBits))
}
42 changes: 42 additions & 0 deletions examples/webcrypto/derive_key/derive-key-pbkdf2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function stringToArrayBuffer(str) {
const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
const bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}

export default async function () {
// create a low entropy password as array of bytes
const password = stringToArrayBuffer("hello world password!")

// import password as CryptoKey
const importedKey = await crypto.subtle.importKey('raw', password, 'pbkdf2', false, [
'deriveBits', 'deriveKey'
]);

// generate random salt
const saltArray = new Uint8Array(16)
crypto.getRandomValues(saltArray)
const salt = saltArray.buffer

// derive AES-GCM 256 key using deriveKey
const derivedKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 25384,
hash: "SHA-256"
},
importedKey,
{
name : "AES-GCM",
length: 256,
},
true,
["encrypt", 'decrypt']
)

console.log(derivedKey)
}
1 change: 1 addition & 0 deletions internal/js/modules/k6/browser/k6ext/k6test/vu.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type WithIteration = int64
// - WithIteration: sets the iteration (default 0).
func (v *VU) StartIteration(tb testing.TB, opts ...any) {
tb.Helper()
tb.Cleanup(func() { v.EndIteration(tb) }) // this prevents leaks. there are no sideeffects if this is done before.
v.iterEvent(tb, event.IterStart, "IterStart", opts...)
}

Expand Down
24 changes: 24 additions & 0 deletions internal/js/modules/k6/webcrypto/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,29 @@ func exportAESKey(key *CryptoKey, format KeyFormat) (interface{}, error) {
}
}

// AESGetLengthParams is the parameters required for algorithms to run the GetLength Operation
type AESGetLengthParams struct {
Algorithm
}

func newAESGetLengthParams(normalized Algorithm) *AESGetLengthParams {
return &AESGetLengthParams{
Algorithm: normalized,
}
}

// GetKeyLength represents the AES function that get the key length from the AES params
func (AESGetLengthParams) GetKeyLength(rt *sobek.Runtime, params sobek.Value) (int, error) {
length, err := traverseObject(rt, params, "length")
if err != nil {
return 0, err
}

keyLengthInt := length.ToInteger()

return int(keyLengthInt), nil
}

// AESImportParams is an internal placeholder struct for AES import parameters.
// Although not described by the specification, we define it to be able to implement
// our internal KeyImporter interface.
Expand All @@ -178,6 +201,7 @@ func (aip *AESImportParams) ImportKey(
format KeyFormat,
keyData []byte,
keyUsages []CryptoKeyUsage,
_ bool,
) (*CryptoKey, error) {
for _, usage := range keyUsages {
switch usage {
Expand Down
27 changes: 26 additions & 1 deletion internal/js/modules/k6/webcrypto/algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const (

// ECDH represents the ECDH algorithm.
ECDH = "ECDH"

// PBKDF2 represents the PBKDF2 algorithm
PBKDF2 = "PBKDF2"
)

// HashAlgorithmIdentifier represents the name of a hash algorithm.
Expand Down Expand Up @@ -116,6 +119,9 @@ const (

// OperationIdentifierDigest represents the digest operation.
OperationIdentifierDigest OperationIdentifier = "digest"

// OperationIdentifierGetKeyLength represents a getKeyLength operation
OperationIdentifierGetKeyLength = "getKeyLength"
)

// normalizeAlgorithm normalizes the given algorithm following the
Expand Down Expand Up @@ -192,11 +198,18 @@ func isRegisteredAlgorithm(algorithmName string, forOperation string) bool {
return isAesAlgorithm(algorithmName) ||
algorithmName == HMAC ||
isEllipticCurve(algorithmName) ||
isRSAAlgorithm(algorithmName)
isRSAAlgorithm(algorithmName) ||
isPBKDF2Algorithm(algorithmName)
case OperationIdentifierEncrypt, OperationIdentifierDecrypt:
return isAesAlgorithm(algorithmName) || algorithmName == RSAOaep
case OperationIdentifierSign, OperationIdentifierVerify:
return algorithmName == HMAC || algorithmName == ECDSA || algorithmName == RSAPss || algorithmName == RSASsaPkcs1v15
case OperationIdentifierDeriveBits:
return isHashAlgorithm(algorithmName) || isPBKDF2Algorithm(algorithmName) || isECDHAlgorithm(algorithmName)
case OperationIdentifierDeriveKey:
return isHashAlgorithm(algorithmName) || isPBKDF2Algorithm(algorithmName)
case OperationIdentifierGetKeyLength:
return isAesAlgorithm(algorithmName)
default:
return false
}
Expand All @@ -214,6 +227,18 @@ func isRSAAlgorithm(algorithmName string) bool {
return algorithmName == RSASsaPkcs1v15 || algorithmName == RSAPss || algorithmName == RSAOaep
}

func isPBKDF2Algorithm(algorithmName string) bool {
return algorithmName == PBKDF2
}

func isECDHAlgorithm(algorithmName string) bool {
return algorithmName == ECDH
}

func isHMACAlgorithm(algorithmName string) bool {
return algorithmName == HMAC
}

// hasAlg an internal interface that helps us to identify
// if a given object has an algorithm method.
type hasAlg interface {
Expand Down
27 changes: 22 additions & 5 deletions internal/js/modules/k6/webcrypto/bits.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
package webcrypto

type bitsDeriver func(CryptoKey, CryptoKey) ([]byte, error)
import "github.com/grafana/sobek"

func newBitsDeriver(algName string) (bitsDeriver, error) {
if algName == ECDH {
return deriveBitsECDH, nil
// BitsDeriver is the interface implemented by the parameters used to derive bits
type BitsDeriver interface {
DeriveBits(rt *sobek.Runtime, baseKey sobek.Value, length int) ([]byte, error)
}

func newBitsDeriver(rt *sobek.Runtime, normalized Algorithm, algorithm sobek.Value) (BitsDeriver, error) {
var deriver BitsDeriver
var err error

switch normalized.Name {
case ECDH:
deriver, err = newECDHKeyDeriveParams(rt, normalized, algorithm)
case PBKDF2:
deriver, err = newPBKDF2DeriveParams(rt, normalized, algorithm)
default:
return nil, NewError(NotSupportedError, "unsupported algorithm for derive bits: "+normalized.Name)
}

if err != nil {
return nil, err
}

return nil, NewError(NotSupportedError, "unsupported algorithm for derive bits: "+algName)
return deriver, nil
}
85 changes: 82 additions & 3 deletions internal/js/modules/k6/webcrypto/elliptic_curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func (e *EcKeyImportParams) ImportKey(
format KeyFormat,
keyData []byte,
_ []CryptoKeyUsage,
_ bool,
) (*CryptoKey, error) {
var importFn func(curve EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error)

Expand Down Expand Up @@ -485,17 +486,95 @@ func extractPublicKeyBytes(alg string, handle any) ([]byte, error) {
return nil, errors.New("unsupported algorithm " + alg)
}

func deriveBitsECDH(privateKey CryptoKey, publicKey CryptoKey) ([]byte, error) {
// ECDHKeyDeriveParams represents the object that should be passed as the algorithm parameter
// into `SubtleCrypto.DeriveBits` or `SubtleCrypto.DeriveKey` when generating any elliptic-curve-based
// key: that is, when the algorithm is identified as ECDH.
type ECDHKeyDeriveParams struct {
Algorithm
Public *CryptoKey
}

var _ BitsDeriver = &ECDHKeyDeriveParams{}

func newECDHKeyDeriveParams(rt *sobek.Runtime, normalized Algorithm, params sobek.Value) (*ECDHKeyDeriveParams, error) {
var publicKey *CryptoKey

pcValue, err := traverseObject(rt, params, "public")
if err != nil {
return nil, NewError(TypeError, "algorithm does not contain a public key")
}
if err := rt.ExportTo(pcValue, &publicKey); err != nil {
return nil, NewError(TypeError, "algorithm's public is not a valid CryptoKey: "+err.Error())
}
if err := publicKey.Validate(); err != nil {
return nil, NewError(TypeError, "algorithm's public key is not a valid CryptoKey: "+err.Error())
}

if publicKey.Type != PublicCryptoKeyType {
return nil, NewError(InvalidAccessError, "algorithm's public key is not a public key")
}

keyAlgorithmNameValue, err := traverseObject(rt, pcValue, "algorithm", "name")
if err != nil {
return nil, err
}

if normalized.Name != keyAlgorithmNameValue.String() {
return nil, NewError(
InvalidAccessError,
"algorithm name does not match public key's algorithm name: "+
normalized.Name+" != "+keyAlgorithmNameValue.String(),
)
}

return &ECDHKeyDeriveParams{
Algorithm: normalized,
Public: publicKey,
}, nil
}

// DeriveBits represents the EC function that derives the key as bits from EC params
func (keyParams ECDHKeyDeriveParams) DeriveBits(rt *sobek.Runtime, baseKey sobek.Value, length int) ([]byte, error) {
var privateKey *CryptoKey

if err := rt.ExportTo(baseKey, &privateKey); err != nil {
return nil, NewError(InvalidAccessError, "provided baseKey is not a valid CryptoKey")
}
if err := privateKey.Validate(); err != nil {
return nil, NewError(InvalidAccessError, "provided baseKey is not a valid CryptoKey: "+err.Error())
}

if privateKey.Type != PrivateCryptoKeyType {
return nil, NewError(InvalidAccessError, fmt.Sprintf("provided baseKey is not a private key: %v", privateKey))
}

if !privateKey.ContainsUsage(DeriveBitsCryptoKeyUsage) {
return nil, NewError(InvalidAccessError, "provided baseKey does not contain the 'deriveBits' usage")
}

if err := ensureKeysUseSameCurve(*privateKey, *keyParams.Public); err != nil {
return nil, NewError(InvalidAccessError, err.Error())
}

pk, ok := privateKey.handle.(*ecdh.PrivateKey)
if !ok {
return nil, NewError(InvalidAccessError, "key is not a valid ECDH private key")
}
pc, ok := publicKey.handle.(*ecdh.PublicKey)
pc, ok := keyParams.Public.handle.(*ecdh.PublicKey)
if !ok {
return nil, NewError(InvalidAccessError, "key is not a valid ECDH public key")
}

return pk.ECDH(pc)
b, err := pk.ECDH(pc)
if err != nil {
return nil, err
}

if len(b) < length/8 {
return nil, NewError(OperationError, "length is too large")
}

return b[:length/8], nil
}

// The ECDSAParams represents the object that should be passed as the algorithm
Expand Down
1 change: 1 addition & 0 deletions internal/js/modules/k6/webcrypto/hmac.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ func (hip *HMACImportParams) ImportKey(
format KeyFormat,
keyData []byte,
keyUsages []CryptoKeyUsage,
_ bool,
) (*CryptoKey, error) {
// 2.
for _, usage := range keyUsages {
Expand Down
Loading
Loading