How to map Windows API CredWrite/CredRead in JNA?

天大地大妈咪最大 提交于 2020-08-25 07:56:38

问题


I'm trying to map CredWrite/CredRead in JNA in order to store a thrid party credential used in my Java application in Windows Credential Manager (OS Windows 10).

Here're the original signatures in C:

// https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx
BOOL CredWrite(
  _In_ PCREDENTIAL Credential,
  _In_ DWORD       Flags
);

// https://msdn.microsoft.com/en-us/library/aa374804(v=vs.85).aspx
BOOL CredRead(
  _In_  LPCTSTR     TargetName,
  _In_  DWORD       Type,
  _In_  DWORD       Flags,
  _Out_ PCREDENTIAL *Credential
);

typedef struct _CREDENTIAL {
  DWORD                 Flags;
  DWORD                 Type;
  LPTSTR                TargetName;
  LPTSTR                Comment;
  FILETIME              LastWritten;
  DWORD                 CredentialBlobSize;
  LPBYTE                CredentialBlob;
  DWORD                 Persist;
  DWORD                 AttributeCount;
  PCREDENTIAL_ATTRIBUTE Attributes;
  LPTSTR                TargetAlias;
  LPTSTR                UserName;
} CREDENTIAL, *PCREDENTIAL;

typedef struct _CREDENTIAL_ATTRIBUTE {
  LPTSTR Keyword;
  DWORD  Flags;
  DWORD  ValueSize;
  LPBYTE Value;
} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;

Here're my maps in Java:

WinCrypt instance = (WinCrypt) Native.loadLibrary("Advapi32", WinCrypt.class, W32APIOptions.DEFAULT_OPTIONS);

public boolean CredWrite(
        CREDENTIAL.ByReference Credential,
        int Flags
        );

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        PointerByReference Credential
        );

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public String TargetName;
    public String Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public byte[] CredentialBlob = new byte[128];
    public int Persist;
    public int AttributeCount;
    public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
    public String TargetAlias;
    public String UserName;

    public static class ByReference extends CREDENTIAL implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(Pointer memory) {
            super(memory);                      // LINE 55
        }
    }

    public CREDENTIAL() {
        super();
    }

    public CREDENTIAL(Pointer memory) {
        super(memory); 
        read();                                 // LINE 65
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Flags",
                "Type",
                "TargetName",
                "Comment",
                "LastWritten",
                "CredentialBlobSize",
                "CredentialBlob",
                "Persist",
                "AttributeCount",
                "Attributes",
                "TargetAlias",
                "UserName"
        });
    }
}

public static class CREDENTIAL_ATTRIBUTE extends Structure {
    public String Keyword;
    public int Flags;
    public int ValueSize;
    public byte[] Value = new byte[128];

    public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Keyword",
                "Flags",
                "ValueSize",
                "Value"
        });
    }
}

First I tried to write a credential to Windows Credential Manager:

String password = "passwordtest";
int cbCreds = 1 + password.length();

CREDENTIAL.ByReference credRef = new CREDENTIAL.ByReference();
credRef.Type = WinCrypt.CRED_TYPE_GENERIC;
credRef.TargetName = "TEST/account";
credRef.CredentialBlobSize = cbCreds;
credRef.CredentialBlob = password.getBytes();
credRef.Persist = WinCrypt.CRED_PERSIST_LOCAL_MACHINE;
credRef.UserName = "administrator";

boolean ok = WinCrypt.instance.CredWrite(credRef, 0);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredWrite() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);

Output of the try to write:

CredWrite() - ok: false, errno: 87, errmsg: The parameter is incorrect.

Then I tried to read an existing credential from Windows Credential Manager:

PointerByReference pref = new PointerByReference();
boolean ok = WinCrypt.instance.CredRead("build-apps", WinCrypt.CRED_TYPE_DOMAIN_PASSWORD, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
CREDENTIAL cred = new CREDENTIAL.ByReference(pref.getPointer());        // LINE 44

Output of the try to read:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Exception in thread "main" java.lang.IllegalArgumentException: Structure exceeds provided memory bounds
    at com.sun.jna.Structure.ensureAllocated(Structure.java:366)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:346)
    at com.sun.jna.Structure.read(Structure.java:552)
    at com.abc.crypt.WinCrypt$CREDENTIAL.<init>(WinCrypt.java:65)
    at com.abc.crypt.WinCrypt$CREDENTIAL$ByReference.<init>(WinCrypt.java:55) 
    at com.abc.crypt.CryptTest.main(CryptTest.java:44)
Caused by: java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=8, offset=200
    at com.sun.jna.Memory.boundsCheck(Memory.java:203)
    at com.sun.jna.Memory$SharedMemory.boundsCheck(Memory.java:87)
    at com.sun.jna.Memory.share(Memory.java:131)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:363)
    ... 5 more

So the try to write failed, the try to read succeeded but failed to create a CREDENTIAL object based on the output.

According to the webpage of CredWrite API, the errno 87 I got in write test is the following error:

ERROR_INVALID_PARAMETER

Certain fields cannot be changed in an existing credential. This error is returned if a field does not match the value in a protected field of the existing credential.

However the value I put in CREDENTIAL instance is a new credential rather than an existing one in the Windows Credential Manager.

Any suggestion or idea on how to fix/improve is appreciated.

===================================

UPDATE AFTER APPLYING FIX:

New CredRead:

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        CREDENTIAL.ByReference Credential
        );

Test for CredRead:

CREDENTIAL.ByReference pref = new CREDENTIAL.ByReference();
boolean ok = WinCrypt.instance.CredRead("TEST/account", WinCrypt.CRED_TYPE_GENERIC, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
System.out.println(String.format("Read username = '%s', password='%S' (%d bytes)\n",
        pref.UserName, pref.CredentialBlob, pref.CredentialBlobSize));

Result:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Read username = 'null', password='NULL' (0 bytes)

I checked how JNA samples in contrib use ByReference on out arg and they are doing in the same way by newing a ByReference and pass to the function.


回答1:


CredRead.PCREDENTIAL should be a CREDENTIAL.ByReference. Using PointerByReference ends up passing in a pointer to a NULL value instead of the expected pointer to CREDENTIAL struct.

CREDENTAL.CredentialBlob needs to be a Pointer or PointerType (probably Memory if you're initializing the block yourself). Using an inline byte array shifts the entire structure by the array size, where the callee is expecting a pointer to a block of memory.

UPDATE

I think I misread the declaration of CredRead().

CredRead should continue to use PointerByReference. Use PointerByReference.getValue() to extract the "returned" pointer value from CredRead() in order to create a new CREDENTIALS instance based on the pointer. PointerByReference.getPointer() gives you the address of the memory allocated to hold the pointer value.

public boolean CredWrite(
    CREDENTIAL Credential,
    int Flags
    );

public boolean CredRead(
    String TargetName,
    int Type,
    int Flags,
    PointerByReference pref
    );

PointerByReference pref = new PointerByReference()
CredRead(name, type, flags, pref);
creds = new Credentials(pref.getValue())



回答2:


If you look at the WIN32 definition of CredRead(), the fourth parameter is of type PCREDENTIAL* i.e. it's a pointer to a pointer. So...

  • you need to pass in the address of a pointer i.e. a 4-byte block of memory.
  • Windows allocates a block of memory to hold the CREDENTIAL structure, then tells you where it is by placing the address of that new memory block in the 4-byte block you passed in.
  • When you dereference your original pointer (the one you passed in to CredRead()), you get another pointer (the 4-byte block), which itself needs to be dereferenced to get to the CREDENTIAL.

Welcome to C :-)

TL;DR: The CREDENTIAL class needs to be defined like this:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;

To call CredRead(), declare it like this:

public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;

and invoke it like this:

PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;

The credential blob is another block of memory allocated by Windows, so you don't need to allocate it yourself, Windows will do it, and will tell you where it is by putting its address in the CredentialBlob field.

Since Windows has allocated these blocks of memory for you, and since it has no way of knowing when you will be finished with them, it's your responsibility to free them. So, the CREDENTIAL constructor keeps a copy of the raw pointer CredRead() gave it, and calls CredFree() in the finalizer, to free that memory. CredFree() is declared like this:

public void CredFree( Pointer cred ) ;

To save a credential, you need to prepare the credential blob in the way that CredWrite() is expecting i.e. by storing a pointer to it in the CREDENTIAL.CredentialBlob field:

// prepare the credential blob
byte[] credBlob = password.getBytes( "UTF-16LE" ) ;
Memory credBlobMem = new Memory( credBlob.length ) ;
credBlobMem.write( 0 , credBlob , 0 , credBlob.length ) ;

// create the credential
CREDENTIAL cred = new CREDENTIAL() ;
cred.Type = CRED_TYPE_GENERIC ;
cred.TargetName = new WString( target ) ;
cred.CredentialBlobSize = (int) credBlobMem.size() ;
cred.CredentialBlob = credBlobMem ;
cred.Persist = CRED_PERSIST_LOCAL_MACHINE ;
cred.UserName = new WString( userName ) ;

// save the credential
boolean rc = WinCrypt.INSTANCE.CredWrite( cred , 0 ) ;
if ( ! rc )
    ... ; // handle the error

As an addendum, all this will run into problems if it's being run under a service account, or any other account that doesn't have a permanent profile. I needed to do this for a job being run via Task Scheduler, using a service account that didn't have interactive login rights, and what happens is:

  • I created a batch file that set the passwords, and ran it via Task Scheduler (so that it runs under the service account, and the passwords go into the correct store)
  • Windows creates a temporary profile (check the Event Log) and the passwords go into that.
  • Another batch file to dump the passwords showed that they had been set successfully.
  • Running the main job works, since the temporary profile is still around, but after 5 or 10 minutes, Windows deletes it, including the passwords you've set :-/, so the next time you run the main job, it fails because the passwords are no longer there.

The solution is to create a permanent profile, ideally by logging in interactively, which only needs to be done once. If you can't do this, it's possible to do it programmatically although you will need admin rights for this.




回答3:


Microsoft provides an MIT-licensed Java library for accessing VSTS tokens. https://github.com/microsoft/vsts-authentication-library-for-java

They provide a JNA mapping to Credential Manager functions and usage here: https://github.com/microsoft/vsts-authentication-library-for-java/tree/master/storage/src/main/java/com/microsoft/alm/storage/windows/internal

Very helpful if you're starting from scratch.




回答4:


Based on taka's answer, but with following additional considerations, I implemented a full sample.

Following additional corrections and aspects have been considered:

  • In CredReadW, the targetName must be of type WString, not String. When writing data to the windows vauld using CredWriteW, also a WString was used already.
  • Simplification: instead of using the getFieldOrder() method, I used the annotation style.
  • Reading Kernel32 GetLastError directly is not recommended, because JNA may invoke other calls that delete the former LastError. As discussed in How to make GetLastError reliably work with JNA? I changed to catching the last error as exception.
  • As already shown by taka, there is no need to add "1+" to the credentialBlobSize - but it's more important to use the real memory or byte[] size, because the string length contains less chars because of UTF-8 encoding, than the resulting memory in UTF-16LE encoding for Windows API functions.

Full sample: (note that it requires JNA library; I was using JNA Version 5.6.0 available at https://github.com/java-native-access/jna)

package at.christoph-bimminger.sample;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;

import com.sun.jna.LastErrorException;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Platform;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.WString;
import com.sun.jna.ptr.PointerByReference;

public class Main {


    public interface WinCrypt extends Library {

        WinCrypt INSTANCE = (WinCrypt) Native.load("Advapi32", WinCrypt.class);

        boolean CredWriteW(CREDENTIAL.ByReference credentialw, int flags) throws LastErrorException;

        boolean CredReadW(WString TargetName, int Type, int Flags, PointerByReference pptr) throws LastErrorException;

        public static final class Type {
            /**
             * The credential is a generic credential. The credential will not be used by
             * any particular authentication package. The credential will be stored securely
             * but has no other significant characteristics.
             */
            final static int CRED_TYPE_GENERIC = 1;

            /**
             * The credential is a password credential and is specific to Microsoft's
             * authentication packages. The NTLM, Kerberos, and Negotiate authentication
             * packages will automatically use this credential when connecting to the named
             * target.
             */
            final static int CRED_TYPE_DOMAIN_PASSWORD = 2;

            /**
             * The credential is a certificate credential and is specific to Microsoft's
             * authentication packages. The Kerberos, Negotiate, and Schannel authentication
             * packages automatically use this credential when connecting to the named
             * target.
             * 
             */
            final static int CRED_TYPE_DOMAIN_CERTIFICATE = 3;

            /**
             * This value is no longer supported. Windows Server 2003 and Windows XP: The
             * credential is a password credential and is specific to authentication
             * packages from Microsoft. The Passport authentication package will
             * automatically use this credential when connecting to the named target.
             * 
             * Additional values will be defined in the future. Applications should be
             * written to allow for credential types they do not understand.
             * 
             */
            final static int CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4;

            /**
             * The credential is a certificate credential that is a generic authentication
             * package. Windows Server 2008, Windows Vista, Windows Server 2003 and Windows
             * XP: This value is not supported.
             */
            final static int CRED_TYPE_GENERIC_CERTIFICATE = 5;

            /**
             * The credential is supported by extended Negotiate packages. Windows Server
             * 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not
             * supported.
             * 
             */
            final static int CRED_TYPE_DOMAIN_EXTENDED = 6;

            /**
             * The maximum number of supported credential types.Windows Server 2008, Windows
             * Vista, Windows Server 2003 and Windows XP: This value is not supported.
             * 
             */
            final static int CRED_TYPE_MAXIMUM = 7;

            final static int CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000;
        }

        public static final class Persist {
            final static int CRED_PERSIST_SESSION = 1;
            final static int CRED_PERSIST_LOCAL_MACHINE = 2;
            final static int CRED_PERSIST_ENTERPRISE = 3;
        }

    }

    /**
     * Representation of native struct FILETIME. See
     * https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
     * 
     * @author Christoph Bimminger
     *
     */
    @FieldOrder({ "dwLowDateTime", "dwHighDateTime" })
    public static final class FILETIME extends Structure {
        public int dwLowDateTime;
        public int dwHighDateTime;
    }

    /**
     * Representation of native struct CREDENTIALW. See
     * https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentialw
     * 
     * @author Christoph Bimminger
     *
     */
    @FieldOrder({ "flags", "type", "targetName", "comment", "lastWritten", "credentialBlobSize", "credentialBlob",
            "persist", "attributeCount", "attributes", "targetAlias", "userName" })
    public static class CREDENTIAL extends Structure {
        public int flags;
        public int type;
        public WString targetName;
        public WString comment;
        public FILETIME lastWritten;
        public int credentialBlobSize = 256;
        public Pointer credentialBlob;
        public int persist;
        public int attributeCount;
        public CREDENTIAL_ATTRIBUTE.ByReference attributes;
        public WString targetAlias;
        public WString userName;

        public static class ByReference extends CREDENTIAL implements Structure.ByReference {
            public ByReference() {
            }

            public ByReference(Pointer memory) {
                super(memory); // LINE 55
            }
        }

        public CREDENTIAL() {
            super();
        }

        public CREDENTIAL(Pointer memory) {
            super(memory);
            read(); // LINE 65
        }

    }

    public static class CREDENTIAL_ATTRIBUTE extends Structure {
        public String Keyword;
        public int Flags;
        public int ValueSize;
        public byte[] Value = new byte[128];

        public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
        }

        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList(new String[] { "Keyword", "Flags", "ValueSize", "Value" });
        }
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        if (!Platform.isWindows())
            throw new UnsatisfiedLinkError("This sample requires a windows environment, it uses wincred.h");

        { // --- SAVE
            String password = "brillant";

            // prepare the credential blob
            byte[] credBlob = password.getBytes("UTF-16LE");
            Memory credBlobMem = new Memory(credBlob.length);
            credBlobMem.write(0, credBlob, 0, credBlob.length);

            int cbCreds = credBlob.length;

            CREDENTIAL.ByReference cred = new CREDENTIAL.ByReference();
            cred.type = WinCrypt.Type.CRED_TYPE_GENERIC;
            cred.targetName = new WString("FOO/account");
            cred.credentialBlobSize = cbCreds;
            cred.credentialBlob = credBlobMem;
            cred.persist = WinCrypt.Persist.CRED_PERSIST_LOCAL_MACHINE;
            cred.userName = new WString("paula");

            try {
                boolean ok = WinCrypt.INSTANCE.CredWriteW(cred, 0);
            } catch (LastErrorException error) {
                int rc = error.getErrorCode();
                String errMsg = error.getMessage();
                System.out.println(rc + ": " + errMsg);
                System.exit(1);

            }
        }

        ///////////////////// READ PASS

        try {
            PointerByReference pptr = new PointerByReference();
            boolean ok = WinCrypt.INSTANCE.CredReadW(new WString("FOO/account"), WinCrypt.Type.CRED_TYPE_GENERIC, 0,
                    pptr);
            CREDENTIAL cred = new CREDENTIAL(pptr.getValue());

            String password = new String(cred.credentialBlob.getByteArray(0, cred.credentialBlobSize), "UTF-16LE");

            System.out.println(password);
        } catch (LastErrorException error) {
            int rc = error.getErrorCode();
            String errMsg = error.getMessage();
            System.out.println(rc + ": " + errMsg);
            System.exit(1);

        }

    }

}


来源:https://stackoverflow.com/questions/38404517/how-to-map-windows-api-credwrite-credread-in-jna

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!